Skip to content

Commit 349972b

Browse files
FoundupFoundups Agent
andauthored
fix(antifafm): redact OBS WebSocket secrets from logs (#720)
Co-authored-by: Foundups Agent <dev@foundups.com>
1 parent d88f97e commit 349972b

10 files changed

Lines changed: 371 additions & 7 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# OBS_WEBSOCKET_SECRET_LOGGING_FIX_PHASE1
2+
3+
## Status
4+
5+
Implemented for W10 review.
6+
7+
## Worker-Lane
8+
9+
0102 implementation lane after W1/W3 audit findings.
10+
11+
## Slice
12+
13+
OBS_WEBSOCKET_SECRET_LOGGING_FIX_PHASE1
14+
15+
## Problem
16+
17+
The AntifaFM OBS WebSocket dependency path could emit OBS connection
18+
parameters through third-party `obsws_python` log records. The runtime audit
19+
found plaintext password fields in local log files. This is a P0 secret
20+
exposure boundary.
21+
22+
This slice prevents future emission of OBS WebSocket passwords,
23+
authentication tokens, and stream keys through console or file logging. It
24+
does not rotate secrets and does not purge historical logs; those are
25+
operational actions outside the code slice.
26+
27+
## Root Cause
28+
29+
`OBS_WEBSOCKET_PASSWORD` is passed into `obsws_python.ReqClient(...)`. The
30+
third-party package may log connection parameters or object representations
31+
at INFO/WARNING levels. Repository root logging writes to stdout and
32+
`logs/foundups_agent.log`, so unredacted third-party records can persist.
33+
34+
## Fix Shape
35+
36+
Added a narrow OBS logging guard:
37+
38+
- redaction filter for OBS password/authentication/key fields
39+
- known `obsws_python` logger suppression above INFO
40+
- guarded `create_obs_req_client()` helper
41+
- root logger installation after `logging.basicConfig(...)`
42+
- replacement of OBS client construction paths that read
43+
`OBS_WEBSOCKET_PASSWORD`
44+
45+
## Files Changed
46+
47+
| File | Change |
48+
|------|--------|
49+
| `main.py` | Install OBS logging guard after root logging setup |
50+
| `modules/platform_integration/antifafm_broadcaster/src/obs_logging_guard.py` | New redaction and client-construction helper |
51+
| `modules/platform_integration/antifafm_broadcaster/src/obs_controller.py` | Use guarded OBS client creation |
52+
| `modules/platform_integration/antifafm_broadcaster/skillz/boot_layer_rotator/executor.py` | Use guarded OBS client creation |
53+
| `modules/platform_integration/antifafm_broadcaster/skillz/news_maps/executor.py` | Use guarded OBS client creation |
54+
| `modules/platform_integration/antifafm_broadcaster/skillz/gcc_shipping_tracker/executor.py` | Use guarded OBS client creation |
55+
| `modules/platform_integration/antifafm_broadcaster/tests/test_obs_logging_guard.py` | New synthetic secret redaction tests |
56+
| `modules/platform_integration/antifafm_broadcaster/ModLog.md` | WSP 22 change entry |
57+
| `modules/platform_integration/antifafm_broadcaster/tests/TestModLog.md` | Test coverage entry |
58+
59+
## Validation Contract
60+
61+
All tests use synthetic secrets only.
62+
63+
No test reads `.env`, prints a real password, connects to OBS, starts OBS,
64+
opens browser automation, or performs a network call.
65+
66+
## Operational Follow-Up
67+
68+
Because local logs already contained plaintext password fields before this
69+
slice, the OBS WebSocket password should be treated as compromised unless it
70+
has been rotated after the affected logs were produced.
71+
72+
Recommended operational actions:
73+
74+
1. Rotate the OBS WebSocket password.
75+
2. Redact or purge local affected log files.
76+
3. Keep the new guard in place to prevent recurrence.
77+
78+
## Truth Boundary Checklist Item
79+
80+
| # | Truth Boundary Checklist Item | Status | Evidence |
81+
|---|-------------------------------|--------|----------|
82+
| 1 | OBS_WEBSOCKET_SECRET_LOGGING_FIX_ONLY | YES | Scope is limited to OBS log redaction and affected client construction paths |
83+
| 2 | DEFAULT_NO_SECRET_LOGGING | YES | Guard installs root/handler filters and suppresses known obsws loggers |
84+
| 3 | SYNTHETIC_SECRET_TESTS_ONLY | YES | New tests use synthetic strings only |
85+
| 4 | NO_ENV_SECRET_READ_IN_TESTS | YES | Tests do not read `.env` or `OBS_WEBSOCKET_PASSWORD` |
86+
| 5 | NO_LIVE_OBS_CONNECTION_IN_TESTS | YES | Tests use a fake OBS module |
87+
| 6 | NO_NETWORK_CALL_IN_TESTS | YES | Tests only exercise logging and fake client construction |
88+
| 7 | NO_SECRET_VALUE_PRINTED | YES | Audit and tests contain no real password values |
89+
| 8 | NO_STREAM_KEY_EXPOSURE | YES | Redaction covers stream key and key fields |
90+
| 9 | HISTORICAL_LOG_ROTATION_NOT_PERFORMED | YES | Operational rotation/purge documented as follow-up |
91+
| 10 | NO_ANTIFAFM_STARTUP_BOUNDARY_CHANGE | YES | Startup auto-launch behavior is deferred to a separate slice |
92+
| 11 | NO_DEPENDENCY_CHANGE | YES | No new dependency added |
93+
| 12 | NO_CI_CHANGE | YES | No workflow files changed |
94+
| 13 | NO_REGISTRY_MUTATION | YES | No registry/catalog/manifest/projection files changed |
95+
| 14 | NO_PUBLIC_ROUTE_ACTIVATION | YES | No public surface changed |
96+
| 15 | NO_CABR_READY | YES | No readiness or governance promotion claimed |
97+
| 16 | NO_PAYOUT_READY | YES | No payout readiness claimed |
98+
| 17 | NO_DAO_ACTIVATION | YES | No DAO activation claimed |
99+
100+
## Next Slice
101+
102+
`MAIN_MENU_ANTIFAFM_STARTUP_BOUNDARY_FIX_PHASE1`
103+
104+
That slice should remove or strictly gate the legacy `ANTIFAFM_AUTO_START`
105+
path that can launch OBS/metadata/rotator before the interactive menu.

main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,15 @@ def _flush_streams():
128128
]
129129
)
130130

131+
try:
132+
from modules.platform_integration.antifafm_broadcaster.src.obs_logging_guard import (
133+
install_obs_logging_guard,
134+
)
135+
136+
install_obs_logging_guard()
137+
except Exception:
138+
pass
139+
131140
# Suppress noisy warnings from optional dependencies during startup
132141
import warnings
133142

modules/platform_integration/antifafm_broadcaster/ModLog.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1665,4 +1665,30 @@ modules/platform_integration/antifafm_broadcaster/
16651665

16661666
---
16671667

1668+
## V1.3.7 - OBS WebSocket Secret Logging Guard (2026-05-26)
1669+
1670+
**Context**: Runtime logs showed OBS WebSocket connection records could include
1671+
the configured password in plaintext through third-party `obsws_python` logging.
1672+
1673+
**Changes**:
1674+
- Added `src/obs_logging_guard.py`
1675+
- redacts OBS WebSocket passwords, authentication values, and stream keys from
1676+
emitted log records
1677+
- raises known `obsws_python` loggers above INFO before client construction
1678+
- provides a guarded `create_obs_req_client()` helper
1679+
- Updated OBS client construction paths that read `OBS_WEBSOCKET_PASSWORD`
1680+
- `src/obs_controller.py`
1681+
- `skillz/boot_layer_rotator/executor.py`
1682+
- `skillz/news_maps/executor.py`
1683+
- `skillz/gcc_shipping_tracker/executor.py`
1684+
- Installed the guard after root logging setup in repository `main.py`.
1685+
1686+
**Impact**:
1687+
- Future OBS WebSocket connection logs redact secrets before console or file
1688+
handlers emit them.
1689+
- Existing logs that already captured secrets still require operational rotation
1690+
and cleanup outside this code change.
1691+
1692+
---
1693+
16681694
*ModLog format per WSP 22*

modules/platform_integration/antifafm_broadcaster/skillz/boot_layer_rotator/executor.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@
2525
from pathlib import Path
2626
from typing import Dict, Any, List, Optional, Callable
2727

28+
from modules.platform_integration.antifafm_broadcaster.src.obs_logging_guard import (
29+
create_obs_req_client,
30+
install_obs_logging_guard,
31+
)
32+
2833
logger = logging.getLogger(__name__)
2934

30-
# Suppress obsws_python password logging (security)
31-
logging.getLogger("obsws_python.baseclient").setLevel(logging.WARNING)
32-
logging.getLogger("obsws_python.reqs").setLevel(logging.WARNING)
35+
install_obs_logging_guard()
3336

3437
# Telemetry path for event logging
3538
TELEMETRY_DIR = Path(__file__).parent.parent.parent / "telemetry"
@@ -210,7 +213,12 @@ def _get_obs_client():
210213
port = int(os.getenv("OBS_WEBSOCKET_PORT", 4455))
211214
password = os.getenv("OBS_WEBSOCKET_PASSWORD", "")
212215

213-
_obs_client = obs.ReqClient(host=host, port=port, password=password)
216+
_obs_client = create_obs_req_client(
217+
obs,
218+
host=host,
219+
port=port,
220+
password=password,
221+
)
214222
logger.info(f"[ROTATOR] OBS WebSocket connected to {host}:{port}")
215223
return _obs_client
216224

modules/platform_integration/antifafm_broadcaster/skillz/gcc_shipping_tracker/executor.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
from pathlib import Path
2929
from typing import Dict, Any, List, Optional
3030

31+
from modules.platform_integration.antifafm_broadcaster.src.obs_logging_guard import (
32+
create_obs_req_client,
33+
)
34+
3135
logger = logging.getLogger(__name__)
3236

3337
# Screenshot cache directory (012 behavior - fetch once, display cached)
@@ -438,7 +442,7 @@ async def update_obs_browser_source(url: str, fallback_on_fail: bool = True) ->
438442
port = int(os.getenv("OBS_WEBSOCKET_PORT", 4455))
439443
password = os.getenv("OBS_WEBSOCKET_PASSWORD", "")
440444

441-
client = obs.ReqClient(host=host, port=port, password=password)
445+
client = create_obs_req_client(obs, host=host, port=port, password=password)
442446

443447
# Browser source name (env var or default to existing)
444448
source_name = os.getenv("OBS_BROWSER_SOURCE", "antifaFM Website")

modules/platform_integration/antifafm_broadcaster/skillz/news_maps/executor.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
from pathlib import Path
3131
from typing import Any, Dict, List, Optional
3232

33+
from modules.platform_integration.antifafm_broadcaster.src.obs_logging_guard import (
34+
create_obs_req_client,
35+
)
36+
3337
logger = logging.getLogger(__name__)
3438

3539
# Cache directories
@@ -298,7 +302,7 @@ async def update_obs_image_source(source_name: str, image_path: Path) -> Dict[st
298302
port = int(os.getenv("OBS_WEBSOCKET_PORT", 4455))
299303
password = os.getenv("OBS_WEBSOCKET_PASSWORD", "")
300304

301-
client = obs.ReqClient(host=host, port=port, password=password)
305+
client = create_obs_req_client(obs, host=host, port=port, password=password)
302306
client.set_input_settings(
303307
name=source_name,
304308
settings={"file": str(image_path.absolute())},

modules/platform_integration/antifafm_broadcaster/src/obs_controller.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,17 @@
2828
import time
2929
from typing import Optional, Dict, Any, Tuple
3030

31+
from modules.platform_integration.antifafm_broadcaster.src.obs_logging_guard import (
32+
create_obs_req_client,
33+
install_obs_logging_guard,
34+
)
35+
3136
logger = logging.getLogger(__name__)
3237

3338
# Use obsws-python (v5 protocol for OBS 28+)
3439
OBS_CLIENT = None
3540
try:
41+
install_obs_logging_guard()
3642
import obsws_python as obs
3743
OBS_CLIENT = "obsws_python"
3844
except ImportError:
@@ -66,7 +72,12 @@ async def connect(self) -> bool:
6672
return False
6773

6874
try:
69-
self.ws = obs.ReqClient(host=self.host, port=self.port, password=self.password)
75+
self.ws = create_obs_req_client(
76+
obs,
77+
host=self.host,
78+
port=self.port,
79+
password=self.password,
80+
)
7081
self.connected = True
7182
logger.info(f"[OBS] Connected to OBS on {self.host}:{self.port}")
7283
return True
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
"""OBS logging guard for third-party WebSocket clients.
2+
3+
The obsws_python package can include connection parameters in log records and
4+
object representations. This module installs a narrow redaction filter and
5+
raises known obsws loggers above INFO before constructing any OBS client.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import logging
11+
import re
12+
from typing import Any
13+
14+
15+
_REDACTED = "<redacted>"
16+
_OBS_LOGGER_NAMES = (
17+
"obsws_python",
18+
"obsws_python.baseclient",
19+
"obsws_python.baseclient.ObsClient",
20+
"obsws_python.reqs",
21+
"obsws_python.reqs.ReqClient",
22+
)
23+
24+
_SECRET_PATTERNS = (
25+
re.compile(r"(?P<prefix>\bpassword\s*=\s*)'[^']*'", re.IGNORECASE),
26+
re.compile(r'(?P<prefix>\bpassword\s*=\s*)"[^"]*"', re.IGNORECASE),
27+
re.compile(r"(?P<prefix>\bpassword\s*=\s*)([^,\s)]+)", re.IGNORECASE),
28+
re.compile(r"(?P<prefix>['\"]password['\"]\s*:\s*)'[^']*'", re.IGNORECASE),
29+
re.compile(r'(?P<prefix>[\'"]password[\'"]\s*:\s*)"[^"]*"', re.IGNORECASE),
30+
re.compile(r"(?P<prefix>['\"]authentication['\"]\s*:\s*)'[^']*'", re.IGNORECASE),
31+
re.compile(r'(?P<prefix>[\'"]authentication[\'"]\s*:\s*)"[^"]*"', re.IGNORECASE),
32+
re.compile(r"(?P<prefix>\bauthentication\s*=\s*)'[^']*'", re.IGNORECASE),
33+
re.compile(r'(?P<prefix>\bauthentication\s*=\s*)"[^"]*"', re.IGNORECASE),
34+
re.compile(r"(?P<prefix>['\"]key['\"]\s*:\s*)'[^']+'", re.IGNORECASE),
35+
re.compile(r'(?P<prefix>[\'"]key[\'"]\s*:\s*)"[^"]+"', re.IGNORECASE),
36+
re.compile(r"(?P<prefix>\bkey\s*=\s*)'[^']+'", re.IGNORECASE),
37+
re.compile(r'(?P<prefix>\bkey\s*=\s*)"[^"]+"', re.IGNORECASE),
38+
re.compile(r"(?P<prefix>\bstream_key\s*=\s*)'[^']+'", re.IGNORECASE),
39+
re.compile(r'(?P<prefix>\bstream_key\s*=\s*)"[^"]+"', re.IGNORECASE),
40+
)
41+
42+
43+
def redact_obs_log_message(value: Any) -> str:
44+
"""Return a log-safe string with OBS credentials redacted."""
45+
text = str(value)
46+
for pattern in _SECRET_PATTERNS:
47+
text = pattern.sub(lambda match: f"{match.group('prefix')}'{_REDACTED}'", text)
48+
return text
49+
50+
51+
class OBSSecretRedactionFilter(logging.Filter):
52+
"""Redact OBS/WebSocket secrets from log records before handlers emit them."""
53+
54+
def filter(self, record: logging.LogRecord) -> bool:
55+
record.msg = redact_obs_log_message(record.getMessage())
56+
record.args = ()
57+
return True
58+
59+
60+
def _has_redaction_filter(target: Any) -> bool:
61+
return any(isinstance(filter_, OBSSecretRedactionFilter) for filter_ in target.filters)
62+
63+
64+
def _attach_filter(target: Any) -> None:
65+
if not _has_redaction_filter(target):
66+
target.addFilter(OBSSecretRedactionFilter())
67+
68+
69+
def install_obs_logging_guard() -> None:
70+
"""Install OBS log redaction and suppress known verbose obsws loggers.
71+
72+
This function is idempotent and safe to call before every OBS client
73+
construction.
74+
"""
75+
root = logging.getLogger()
76+
_attach_filter(root)
77+
for handler in root.handlers:
78+
_attach_filter(handler)
79+
80+
for logger_name in _OBS_LOGGER_NAMES:
81+
obs_logger = logging.getLogger(logger_name)
82+
obs_logger.setLevel(logging.WARNING)
83+
_attach_filter(obs_logger)
84+
for handler in obs_logger.handlers:
85+
_attach_filter(handler)
86+
87+
88+
def create_obs_req_client(obs_module: Any, *, host: str, port: int, password: str) -> Any:
89+
"""Create an obsws_python ReqClient after installing the logging guard."""
90+
install_obs_logging_guard()
91+
return obs_module.ReqClient(host=host, port=port, password=password)

modules/platform_integration/antifafm_broadcaster/tests/TestModLog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,12 @@ dropdown_verified = verify_dropdown_appeared(driver, timeout=5)
288288

289289
## Planned Tests
290290

291+
### `test_obs_logging_guard.py`
292+
- Verifies synthetic OBS WebSocket passwords are redacted from formatted log records.
293+
- Verifies `obsws_python` logger levels are raised above INFO.
294+
- Verifies guarded OBS client construction redacts constructor-time third-party logs.
295+
- Uses synthetic secrets only; does not read `.env`, connect to OBS, or run network calls.
296+
291297
### `test_ffmpeg_stream.py` (TODO)
292298
- Test FFmpeg command generation
293299
- Test RTMP connection to YouTube

0 commit comments

Comments
 (0)