Skip to content

Commit ea0dd0e

Browse files
committed
fix: remediate vulnerabilities and sync new shared config utlities
1 parent 6cec3a2 commit ea0dd0e

File tree

8 files changed

+227
-46
lines changed

8 files changed

+227
-46
lines changed

.github/workflows/Create Greetings on Pull Request.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
pull-requests: write
1616
steps:
1717
- name: Greet First-Time Contributors
18-
uses: actions/first-interaction@v1
18+
uses: actions/first-interaction@v2
1919
with:
2020
repo-token: ${{ secrets.GITHUB_TOKEN }}
2121
issue-message: |
Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,37 @@
11
---
22
name: Secret Scanning
3-
on: [push, pull_request]
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
schedule:
8+
- cron: 0 3 * * 0 # weekly full scan on Sunday at 3 AM UTC
49
jobs:
5-
secret-scan:
10+
# 🧪 PR Diff Scan
11+
diff-scan:
12+
name: TruffleHog PR Diff Scan
613
runs-on: ubuntu-latest
14+
if: github.event_name == 'pull_request'
715
permissions:
816
contents: read
917
steps:
1018
- uses: actions/checkout@v4
11-
- name: Scan for Secrets
19+
- name: Run TruffleHog on PR diff
1220
uses: trufflesecurity/trufflehog@main
1321
with:
14-
path: . # Scan the entire repo
22+
base: ${{ github.event.pull_request.base.sha }}
23+
head: ${{ github.event.pull_request.head.sha }}
24+
25+
# 🔍 Full Repo Scan
26+
full-scan:
27+
name: TruffleHog Full Repo Scan
28+
runs-on: ubuntu-latest
29+
if: github.event_name == 'push' || github.event_name == 'schedule'
30+
permissions:
31+
contents: read
32+
steps:
33+
- uses: actions/checkout@v4
34+
- name: Run TruffleHog on entire repo
35+
uses: trufflesecurity/trufflehog@main
36+
with:
37+
path: .

src/app/config_shared.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
"""
66

77
from functools import lru_cache
8-
from typing import List, Tuple
98

109
from app.utils.config_utils import get_config_bool
1110
from app.utils.types import OutputMode
@@ -272,6 +271,16 @@ def get_log_level() -> str:
272271
return get_config_value_cached("LOG_LEVEL", "INFO")
273272

274273

274+
def get_log_format() -> str:
275+
"""Returns the configured log format.
276+
277+
Returns:
278+
str: 'json' or 'text' (default is 'text').
279+
280+
"""
281+
return get_config_value_cached("LOG_FORMAT", "text").lower()
282+
283+
275284
@lru_cache
276285
def get_poller_type() -> str:
277286
"""Retrieve the type/category of this poller.
@@ -1161,7 +1170,7 @@ def get_healthcheck_host() -> str:
11611170
Defaults to '0.0.0.0' if not set.
11621171
11631172
"""
1164-
return get_config_value_cached("HEALTHCHECK_HOST", "0.0.0.0")
1173+
return get_config_value_cached("HEALTHCHECK_HOST", "127.0.0.1")
11651174

11661175

11671176
@lru_cache
@@ -1201,7 +1210,7 @@ def get_metrics_bind_address() -> str:
12011210
Defaults to '0.0.0.0' if not set.
12021211
12031212
"""
1204-
return get_config_value_cached("METRICS_BIND_ADDRESS", "0.0.0.0")
1213+
return get_config_value_cached("METRICS_BIND_ADDRESS", "127.0.0.1")
12051214

12061215

12071216
@lru_cache

src/app/output_handler.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
record_paper_trade_metrics,
2020
record_sink_metrics,
2121
)
22+
from app.utils.redactor import redact_dict
2223
from app.utils.setup_logger import setup_logger
2324
from app.utils.types import OutputMode, validate_list_of_dicts
2425

@@ -115,7 +116,7 @@ def _output_to_log(self, data: list[dict[str, Any]]) -> None:
115116
116117
"""
117118
for item in data:
118-
logger.info("📝 Processed message:\n%s", json.dumps(item, indent=4))
119+
logger.info("📝 Processed message:\n%s", json.dumps(redact_dict(item), indent=4))
119120

120121
def _output_to_stdout(self, data: list[dict[str, Any]]) -> None:
121122
"""Print each item in the data list to standard output.
@@ -224,7 +225,7 @@ def _output_paper_trade_to_queue(self, data: dict[str, Any]) -> None:
224225
queue_name = config_shared.get_paper_trading_queue_name()
225226
exchange = config_shared.get_paper_trading_exchange()
226227
publish_to_queue([data], queue=queue_name, exchange=exchange)
227-
logger.info("🪙 Paper trade sent to queue:\n%s", json.dumps(data, indent=4))
228+
logger.info("🪙 Paper trade sent to queue:\n%s", json.dumps(redact_dict(data), indent=4))
228229
record_paper_trade_metrics("queue", success=True, duration_sec=0)
229230

230231
def _output_paper_trade_to_database(self, data: dict[str, Any]) -> None:

src/app/utils/redactor.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# src/app/utils/redactor.py
2+
3+
"""Utility for redacting sensitive fields from dictionaries before logging.
4+
5+
This module is used to ensure that no passwords, tokens, or other secrets
6+
are ever written to logs in clear-text.
7+
"""
8+
9+
from typing import Any
10+
11+
# Fields that should never appear in logs
12+
SENSITIVE_KEYS = {
13+
"password",
14+
"secret",
15+
"token",
16+
"api_key",
17+
"authorization",
18+
"access_token",
19+
}
20+
21+
22+
def redact_dict(obj: Any) -> Any:
23+
"""Recursively redacts sensitive keys in a dictionary.
24+
25+
Args:
26+
obj (Any): The input data (typically a dict or list of dicts).
27+
28+
Returns:
29+
Any: The redacted version of the input, preserving structure.
30+
31+
"""
32+
if isinstance(obj, dict):
33+
return {
34+
k: "***REDACTED***" if k.lower() in SENSITIVE_KEYS else redact_dict(v)
35+
for k, v in obj.items()
36+
}
37+
elif isinstance(obj, list):
38+
return [redact_dict(item) for item in obj]
39+
else:
40+
return obj

src/app/utils/safe_logger.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Wrapper around the standard logger that applies redaction and structured logging.
2+
3+
This ensures that sensitive fields are redacted and logs follow consistent formatting.
4+
"""
5+
6+
import logging
7+
import os
8+
from typing import Any, Optional
9+
10+
from app.utils.redactor import redact_dict
11+
from app.utils.setup_logger import setup_logger
12+
13+
# Controls whether to log full payloads (only in dev/test)
14+
SAFE_LOG_FULL: bool = os.getenv("SAFE_LOG_FULL", "false").lower() == "true"
15+
SAFE_LOG_STRUCTURED: bool = os.getenv("SAFE_LOG_STRUCTURED", "false").lower() == "true"
16+
17+
# Base logger instance
18+
logger: logging.Logger = setup_logger(__name__, structured=SAFE_LOG_STRUCTURED)
19+
20+
21+
def safe_info(message: str, data: Optional[dict[str, Any]] = None) -> None:
22+
"""
23+
Logs an info-level message with optional sanitized payload metadata.
24+
25+
Args:
26+
message (str): Human-readable log message.
27+
data (Optional[dict]): Dictionary payload to log. Only logs redacted metadata unless SAFE_LOG_FULL is enabled.
28+
"""
29+
if data is None:
30+
logger.info(message)
31+
return
32+
33+
payload_size = len(data) if SAFE_LOG_FULL else len(redact_dict(data))
34+
logger.info("%s | payload_size=%d", message, payload_size)
35+
36+
37+
def safe_warning(message: str, data: Optional[dict[str, Any]] = None) -> None:
38+
"""
39+
Logs a warning-level message with optional sanitized payload metadata.
40+
41+
Args:
42+
message (str): Human-readable log message.
43+
data (Optional[dict]): Dictionary payload to log. Only logs redacted metadata unless SAFE_LOG_FULL is enabled.
44+
"""
45+
if data is None:
46+
logger.warning(message)
47+
return
48+
49+
payload_size = len(data) if SAFE_LOG_FULL else len(redact_dict(data))
50+
logger.warning("%s | payload_size=%d", message, payload_size)
51+
52+
53+
def safe_error(message: str, data: Optional[dict[str, Any]] = None) -> None:
54+
"""
55+
Logs an error-level message with optional sanitized payload metadata.
56+
57+
Args:
58+
message (str): Human-readable log message.
59+
data (Optional[dict]): Dictionary payload to log. Only logs redacted metadata unless SAFE_LOG_FULL is enabled.
60+
"""
61+
if data is None:
62+
logger.error(message)
63+
return
64+
65+
payload_size = len(data) if SAFE_LOG_FULL else len(redact_dict(data))
66+
logger.error("%s | payload_size=%d", message, payload_size)
67+
68+
69+
def safe_debug(message: str, data: Optional[dict[str, Any]] = None) -> None:
70+
"""
71+
Logs a debug-level message with optional sanitized payload metadata.
72+
73+
Args:
74+
message (str): Human-readable log message.
75+
data (Optional[dict]): Dictionary payload to log. Only logs redacted metadata unless SAFE_LOG_FULL is enabled.
76+
"""
77+
if data is None:
78+
logger.debug(message)
79+
return
80+
81+
payload_size = len(data) if SAFE_LOG_FULL else len(redact_dict(data))
82+
logger.debug("%s | payload_size=%d", message, payload_size)

src/app/utils/setup_logger.py

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,81 @@
1+
"""Configures and returns a logger with console, optional file, and optional JSON output.
2+
Supports redaction toggle from config_shared and multi-handler output.
3+
"""
4+
15
import logging
26
import sys
37
from logging import Logger
8+
from logging.handlers import RotatingFileHandler
9+
10+
try:
11+
from pythonjsonlogger.json import JsonFormatter
12+
except ImportError:
13+
JsonFormatter = None # JSON logging fallback
14+
15+
from app import config_shared
416

517

618
def setup_logger(
719
name: str | None = None,
8-
level: int = logging.INFO,
9-
structured: bool = False,
20+
level: int | None = None,
21+
structured: bool | None = None,
22+
log_file: str | None = None,
1023
) -> Logger:
11-
"""Configure and return a logger with optional redaction and structured logging.
24+
"""Configure and return a logger with optional structured and file output.
1225
1326
Args:
1427
name (Optional[str]): Logger name.
15-
level (int): Log level (e.g., logging.INFO).
16-
structured (bool): Enable structured (JSON) logging. (Not yet implemented)
28+
level (Optional[int]): Logging level (overrides LOG_LEVEL config).
29+
structured (Optional[bool]): Use structured (JSON) logging (overrides LOG_FORMAT config).
30+
log_file (Optional[str]): Path to a log file (enables rotation if set).
1731
1832
Returns:
1933
Logger: Configured logger instance.
2034
2135
"""
22-
logger = logging.getLogger(name)
23-
if logger.handlers:
36+
logger = logging.getLogger(name or "app")
37+
38+
if logger.hasHandlers():
2439
return logger
2540

26-
# Delay import to avoid circular dependency
27-
try:
28-
from app import config_shared
41+
# Resolve redaction
42+
redact_enabled = config_shared.get_redact_sensitive_logs()
43+
44+
# Resolve level
45+
level_name = config_shared.get_log_level()
46+
resolved_level: int = level if level is not None else getattr(logging, level_name, logging.INFO)
47+
48+
# Resolve structured format
49+
structured = structured if structured is not None else config_shared.get_log_format() == "json"
50+
51+
# Choose formatter
52+
if structured and JsonFormatter:
53+
formatter = JsonFormatter("%(asctime)s %(levelname)s %(name)s %(message)s")
54+
else:
55+
formatter = logging.Formatter(
56+
fmt="%(asctime)s [%(levelname)s] %(name)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
57+
)
2958

30-
redact = config_shared.get_redact_sensitive_logs()
31-
except Exception:
32-
redact = False
59+
# Console handler
60+
stream_handler = logging.StreamHandler(sys.stdout)
61+
stream_handler.setFormatter(formatter)
62+
logger.addHandler(stream_handler)
3363

34-
formatter = logging.Formatter(
35-
fmt="%(asctime)s [%(levelname)s] %(name)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
36-
)
64+
# Optional rotating file handler
65+
if log_file:
66+
file_handler = RotatingFileHandler(log_file, maxBytes=5 * 1024 * 1024, backupCount=3)
67+
file_handler.setFormatter(formatter)
68+
logger.addHandler(file_handler)
3769

38-
handler = logging.StreamHandler(sys.stdout)
39-
handler.setFormatter(formatter)
40-
logger.addHandler(handler)
41-
logger.setLevel(level)
70+
logger.setLevel(resolved_level)
71+
logger.propagate = False
4272

43-
if redact:
73+
if redact_enabled:
4474
logger.info("🔒 Redaction of sensitive data is ENABLED")
4575
else:
4676
logger.info("🔓 Redaction of sensitive data is DISABLED")
4777

48-
# NOTE: 'structured' is accepted but not yet implemented
49-
if structured:
50-
logger.warning("⚠️ Structured logging requested but not yet supported.")
78+
if structured and not JsonFormatter:
79+
logger.warning("⚠️ Structured logging requested but 'python-json-logger' is not installed.")
5180

5281
return logger

0 commit comments

Comments
 (0)