mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-09-23 01:10:42 +02:00
Close tg12 outbound audit (#348-#366): operator UA, opt-ins, docs
- User-Agent is per-install handle only (no Shadowbroker product token) - LiveUAMap: Windows UI consent when enabling Global Incidents; env override - Meshtastic callsign upstream header off by default (opt-in true) - Expanded docs/OUTBOUND_DATA.md and README link for CCTV, basemap, Broadcastify Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
"""LiveUAMap scraper UI opt-in on Windows (#348)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from services import liveuamap_settings as settings
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def opt_in_file(tmp_path, monkeypatch):
|
||||
path = tmp_path / "liveuamap_scraper_opt_in.json"
|
||||
monkeypatch.setattr(settings, "_OPT_IN_FILE", path)
|
||||
return path
|
||||
|
||||
|
||||
def test_windows_defaults_off_without_opt_in(monkeypatch, opt_in_file):
|
||||
monkeypatch.setattr(settings.os, "name", "nt")
|
||||
monkeypatch.delenv("SHADOWBROKER_ENABLE_LIVEUAMAP_SCRAPER", raising=False)
|
||||
assert settings.liveuamap_scraper_enabled() is False
|
||||
assert settings.liveuamap_requires_ui_opt_in() is True
|
||||
|
||||
|
||||
def test_windows_opt_in_enables_scraper(monkeypatch, opt_in_file):
|
||||
monkeypatch.setattr(settings.os, "name", "nt")
|
||||
monkeypatch.delenv("SHADOWBROKER_ENABLE_LIVEUAMAP_SCRAPER", raising=False)
|
||||
settings.set_liveuamap_ui_opt_in(True)
|
||||
assert settings.liveuamap_scraper_enabled() is True
|
||||
assert json.loads(opt_in_file.read_text())["opted_in"] is True
|
||||
|
||||
|
||||
def test_linux_enabled_without_opt_in(monkeypatch, opt_in_file):
|
||||
monkeypatch.setattr(settings.os, "name", "posix")
|
||||
monkeypatch.delenv("SHADOWBROKER_ENABLE_LIVEUAMAP_SCRAPER", raising=False)
|
||||
assert settings.liveuamap_requires_ui_opt_in() is False
|
||||
assert settings.liveuamap_scraper_enabled() is True
|
||||
|
||||
|
||||
def test_env_force_off_overrides_ui_opt_in(monkeypatch, opt_in_file):
|
||||
monkeypatch.setattr(settings.os, "name", "nt")
|
||||
monkeypatch.setenv("SHADOWBROKER_ENABLE_LIVEUAMAP_SCRAPER", "false")
|
||||
settings.set_liveuamap_ui_opt_in(True)
|
||||
assert settings.liveuamap_scraper_enabled() is False
|
||||
@@ -1,56 +1,27 @@
|
||||
"""Issue #203 (tg12): meshtastic_map.py was unconditionally including
|
||||
``MESHTASTIC_OPERATOR_CALLSIGN`` in the outbound User-Agent header,
|
||||
which contradicted the README's "no user data transmitted" claim.
|
||||
|
||||
The fix preserves the existing default behavior (callsign sent — that's
|
||||
what operators who configured the variable expected) but adds an
|
||||
opt-out env var ``MESHTASTIC_SEND_CALLSIGN_HEADER=false`` for
|
||||
privacy-conscious operators.
|
||||
"""
|
||||
import importlib
|
||||
import sys
|
||||
"""Issue #350: Meshtastic callsign in outbound UA is opt-in, not default."""
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def _reload_meshtastic_module():
|
||||
"""Reload meshtastic_map so settings are re-read on demand."""
|
||||
if "services.fetchers.meshtastic_map" in sys.modules:
|
||||
del sys.modules["services.fetchers.meshtastic_map"]
|
||||
return importlib.import_module("services.fetchers.meshtastic_map")
|
||||
def _send_callsign_header_from_env() -> bool:
|
||||
raw = str(os.environ.get("MESHTASTIC_SEND_CALLSIGN_HEADER", "false")).strip().lower()
|
||||
return raw in {"1", "true", "yes", "on"}
|
||||
|
||||
|
||||
def test_default_behavior_includes_callsign(monkeypatch):
|
||||
"""Operators who set the callsign and don't change anything else
|
||||
keep their existing behavior (callsign sent in UA)."""
|
||||
# We test the UA construction logic by exercising the same branches
|
||||
# the fetcher uses. Direct fetch isn't run because it makes a real
|
||||
# network call — we just verify the env-var-driven decision.
|
||||
import os
|
||||
def test_default_does_not_send_callsign(monkeypatch):
|
||||
monkeypatch.setenv("MESHTASTIC_OPERATOR_CALLSIGN", "N0CALL")
|
||||
monkeypatch.delenv("MESHTASTIC_SEND_CALLSIGN_HEADER", raising=False)
|
||||
|
||||
raw = str(os.environ.get("MESHTASTIC_SEND_CALLSIGN_HEADER", "true")).strip().lower()
|
||||
send_callsign_header = raw not in {"0", "false", "no", "off", ""}
|
||||
assert send_callsign_header is True
|
||||
assert _send_callsign_header_from_env() is False
|
||||
|
||||
|
||||
def test_opt_out_suppresses_callsign(monkeypatch):
|
||||
"""Setting MESHTASTIC_SEND_CALLSIGN_HEADER=false suppresses the header."""
|
||||
import os
|
||||
def test_opt_in_sends_callsign(monkeypatch):
|
||||
monkeypatch.setenv("MESHTASTIC_OPERATOR_CALLSIGN", "N0CALL")
|
||||
monkeypatch.setenv("MESHTASTIC_SEND_CALLSIGN_HEADER", "false")
|
||||
|
||||
raw = str(os.environ.get("MESHTASTIC_SEND_CALLSIGN_HEADER", "true")).strip().lower()
|
||||
send_callsign_header = raw not in {"0", "false", "no", "off", ""}
|
||||
assert send_callsign_header is False
|
||||
monkeypatch.setenv("MESHTASTIC_SEND_CALLSIGN_HEADER", "true")
|
||||
assert _send_callsign_header_from_env() is True
|
||||
|
||||
|
||||
def test_various_falsy_values_all_opt_out(monkeypatch):
|
||||
"""Common falsy strings should all suppress the callsign header."""
|
||||
import os
|
||||
for falsy in ("0", "false", "FALSE", "no", "off"):
|
||||
def test_various_falsy_values_do_not_opt_in(monkeypatch):
|
||||
for falsy in ("0", "false", "FALSE", "no", "off", ""):
|
||||
monkeypatch.setenv("MESHTASTIC_SEND_CALLSIGN_HEADER", falsy)
|
||||
raw = str(os.environ.get("MESHTASTIC_SEND_CALLSIGN_HEADER", "true")).strip().lower()
|
||||
send_callsign_header = raw not in {"0", "false", "no", "off", ""}
|
||||
assert send_callsign_header is False, f"value {falsy!r} did not opt out"
|
||||
assert _send_callsign_header_from_env() is False, f"value {falsy!r} should not opt in"
|
||||
|
||||
@@ -133,23 +133,19 @@ class TestOperatorHandleGeneration:
|
||||
|
||||
|
||||
class TestOutboundUserAgentString:
|
||||
def test_includes_operator_handle(self, isolated_handle):
|
||||
def test_ua_is_operator_handle(self, isolated_handle):
|
||||
ua = isolated_handle.outbound_user_agent()
|
||||
handle = isolated_handle.get_operator_handle()
|
||||
assert f"operator: {handle}" in ua
|
||||
assert ua == handle
|
||||
|
||||
def test_includes_purpose_when_provided(self, isolated_handle):
|
||||
ua = isolated_handle.outbound_user_agent("wikipedia")
|
||||
assert "purpose: wikipedia" in ua
|
||||
handle = isolated_handle.get_operator_handle()
|
||||
assert ua == f"{handle} (purpose: wikipedia)"
|
||||
|
||||
def test_includes_contact_path(self, isolated_handle):
|
||||
ua = isolated_handle.outbound_user_agent()
|
||||
assert "github.com" in ua.lower()
|
||||
assert "shadowbroker" in ua.lower()
|
||||
|
||||
def test_version_prefix(self, isolated_handle):
|
||||
ua = isolated_handle.outbound_user_agent()
|
||||
assert ua.startswith("Shadowbroker/")
|
||||
def test_no_shadowbroker_product_token(self, isolated_handle):
|
||||
ua = isolated_handle.outbound_user_agent("nominatim")
|
||||
assert "shadowbroker" not in ua.lower()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -181,8 +177,8 @@ class TestWikimediaCallsAreNowPerOperator:
|
||||
assert "Api-User-Agent" in headers
|
||||
handle = isolated_handle.get_operator_handle()
|
||||
for header_value in (headers["User-Agent"], headers["Api-User-Agent"]):
|
||||
assert f"operator: {handle}" in header_value, (
|
||||
f"Wikimedia UA must include the per-operator handle; got {header_value!r}"
|
||||
assert header_value.startswith(handle), (
|
||||
f"Wikimedia UA must be the per-operator handle; got {header_value!r}"
|
||||
)
|
||||
|
||||
def test_wikipedia_summary_uses_per_operator_ua(self, isolated_handle, monkeypatch):
|
||||
@@ -211,7 +207,8 @@ class TestWikimediaCallsAreNowPerOperator:
|
||||
assert wikipedia_hits, "Wikipedia summary fetch was not called"
|
||||
for _url, headers in wikipedia_hits:
|
||||
handle = isolated_handle.get_operator_handle()
|
||||
assert f"operator: {handle}" in headers.get("User-Agent", "")
|
||||
ua = headers.get("User-Agent", "")
|
||||
assert ua.startswith(handle), f"Wikipedia UA must be the operator handle; got {ua!r}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -233,6 +230,7 @@ class TestNoMonsterUserAgentRemains:
|
||||
"""
|
||||
|
||||
BANNED_LITERALS = (
|
||||
"Shadowbroker/",
|
||||
"ShadowBroker-OSINT/1.0",
|
||||
"ShadowBroker-OSINT/0.9",
|
||||
"ShadowBroker-FeedIngester/1.0",
|
||||
|
||||
Reference in New Issue
Block a user