mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-16 18:23:32 +00:00
fix: address Bandit B108/B110 across production code
B110 (try/except/pass): - display_controller.py: narrow 'except Exception' to 'except AttributeError' for get_offset_frame() — plugins not having this optional method is the expected case, not all exceptions - config_manager.py: B110 already resolved by the earlier removal of the dead secrets-loading block (the except/pass was inside it) - All other except/pass blocks in src/ and web_interface/ are intentional (last-resort recovery, best-effort fallbacks, non-critical startup probes). Annotated each with # nosec B110 and a brief inline reason so the decision is explicit for future reviewers. - Test files and plugin-repos B110 suppressed via Codacy API (not prod code). B108 (/tmp usage): - permission_utils.py: /tmp listed to PREVENT permission changes on it — not used as a temp path. Annotated # nosec B108. - display_manager.py: fixed snapshot path is intentional (web UI reads same path); path-check guard also annotated. - wifi_manager.py: named /tmp files match the sudoers allowlist installed with the system (the paths are hard-coded in both places by design). Annotated all six open/cp references # nosec B108. - scripts/render_plugin.py: dev script default overridable by user. Annotated. - web_interface/app.py: reads the same fixed path written by display_manager. Annotated # nosec B108. - Test files suppressed via Codacy API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,7 +81,7 @@ def main() -> int:
|
|||||||
help='Plugin config as JSON string')
|
help='Plugin config as JSON string')
|
||||||
parser.add_argument('--mock-data', '-m', default=None,
|
parser.add_argument('--mock-data', '-m', default=None,
|
||||||
help='Path to JSON file with mock cache data')
|
help='Path to JSON file with mock cache data')
|
||||||
parser.add_argument('--output', '-o', default='/tmp/plugin_render.png',
|
parser.add_argument('--output', '-o', default='/tmp/plugin_render.png', # nosec B108 - dev script default; user can override
|
||||||
help='Output PNG path (default: /tmp/plugin_render.png)')
|
help='Output PNG path (default: /tmp/plugin_render.png)')
|
||||||
parser.add_argument('--width', type=int, default=128, help='Display width (default: 128)')
|
parser.add_argument('--width', type=int, default=128, help='Display width (default: 128)')
|
||||||
parser.add_argument('--height', type=int, default=32, help='Display height (default: 32)')
|
parser.add_argument('--height', type=int, default=32, help='Display height (default: 32)')
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ class SportsCore(ABC):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
fallbacks.append(Path.home() / ".ledmatrix" / "logos" / self.sport_key)
|
fallbacks.append(Path.home() / ".ledmatrix" / "logos" / self.sport_key)
|
||||||
except Exception:
|
except Exception: # nosec B110 - Path.home() raises RuntimeError for service users; fallback list continues
|
||||||
pass
|
pass
|
||||||
|
|
||||||
fallbacks.append(Path(tempfile.gettempdir()) / "ledmatrix_logos" / self.sport_key)
|
fallbacks.append(Path(tempfile.gettempdir()) / "ledmatrix_logos" / self.sport_key)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
# System directories that should never have their permissions modified
|
# System directories that should never have their permissions modified
|
||||||
# These directories have special system-level permissions that must be preserved
|
# These directories have special system-level permissions that must be preserved
|
||||||
PROTECTED_SYSTEM_DIRECTORIES = {
|
PROTECTED_SYSTEM_DIRECTORIES = { # nosec B108 - these are checked to PREVENT permission changes, not to use as temp paths
|
||||||
'/tmp',
|
'/tmp',
|
||||||
'/var/tmp',
|
'/var/tmp',
|
||||||
'/dev',
|
'/dev',
|
||||||
|
|||||||
@@ -813,8 +813,8 @@ class DisplayController:
|
|||||||
# 1. Explicit hook — plugin opted in with get_offset_frame()
|
# 1. Explicit hook — plugin opted in with get_offset_frame()
|
||||||
try:
|
try:
|
||||||
follower_frame = plugin_instance.get_offset_frame(offset)
|
follower_frame = plugin_instance.get_offset_frame(offset)
|
||||||
except Exception:
|
except AttributeError:
|
||||||
pass
|
pass # Most plugins don't implement get_offset_frame; that's expected
|
||||||
|
|
||||||
# 2. Auto-detect — plugin has a scroll_helper (standard pattern for all
|
# 2. Auto-detect — plugin has a scroll_helper (standard pattern for all
|
||||||
# scroll plugins). Works with zero plugin code changes.
|
# scroll plugins). Works with zero plugin code changes.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DisplayManager:
|
|||||||
# When True, update_display() and clear() skip hardware writes (used during off-screen content capture)
|
# When True, update_display() and clear() skip hardware writes (used during off-screen content capture)
|
||||||
self._capture_mode_active = False
|
self._capture_mode_active = False
|
||||||
# Snapshot settings for web preview integration (service writes, web reads)
|
# Snapshot settings for web preview integration (service writes, web reads)
|
||||||
self._snapshot_path = "/tmp/led_matrix_preview.png"
|
self._snapshot_path = "/tmp/led_matrix_preview.png" # nosec B108 - fixed path intentional; web UI reads same path
|
||||||
self._snapshot_min_interval_sec = 0.2 # max ~5 fps
|
self._snapshot_min_interval_sec = 0.2 # max ~5 fps
|
||||||
self._last_snapshot_ts = 0.0
|
self._last_snapshot_ts = 0.0
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ class DisplayManager:
|
|||||||
self.draw.rectangle([0, 0, fallback_width - 1, fallback_height - 1], outline=(255, 0, 0))
|
self.draw.rectangle([0, 0, fallback_width - 1, fallback_height - 1], outline=(255, 0, 0))
|
||||||
self.draw.line([0, 0, fallback_width - 1, fallback_height - 1], fill=(0, 255, 0))
|
self.draw.line([0, 0, fallback_width - 1, fallback_height - 1], fill=(0, 255, 0))
|
||||||
self.draw.text((2, max(0, (fallback_height // 2) - 4)), "Simulation", fill=(0, 128, 255))
|
self.draw.text((2, max(0, (fallback_height // 2) - 4)), "Simulation", fill=(0, 128, 255))
|
||||||
except Exception:
|
except Exception: # nosec B110 - best-effort fallback visualization; drawing errors must not crash startup
|
||||||
# Best-effort; ignore drawing errors in fallback
|
# Best-effort; ignore drawing errors in fallback
|
||||||
pass
|
pass
|
||||||
logger.error(f"Matrix initialization failed, using fallback mode with size {fallback_width}x{fallback_height}. Error: {e}")
|
logger.error(f"Matrix initialization failed, using fallback mode with size {fallback_width}x{fallback_height}. Error: {e}")
|
||||||
@@ -894,7 +894,7 @@ class DisplayManager:
|
|||||||
# Never modify /tmp permissions - it has special system permissions (1777)
|
# Never modify /tmp permissions - it has special system permissions (1777)
|
||||||
# that must not be changed or it breaks apt and other system tools
|
# that must not be changed or it breaks apt and other system tools
|
||||||
parent_dir = snapshot_path_obj.parent
|
parent_dir = snapshot_path_obj.parent
|
||||||
if parent_dir and str(parent_dir) != '/tmp':
|
if parent_dir and str(parent_dir) != '/tmp': # nosec B108 - guard to skip /tmp for permission ops
|
||||||
ensure_directory_permissions(parent_dir, get_assets_dir_mode())
|
ensure_directory_permissions(parent_dir, get_assets_dir_mode())
|
||||||
# Write atomically: temp then replace
|
# Write atomically: temp then replace
|
||||||
tmp_path = f"{self._snapshot_path}.tmp"
|
tmp_path = f"{self._snapshot_path}.tmp"
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ class StateReconciliation:
|
|||||||
'version': manifest.get('version'),
|
'version': manifest.get('version'),
|
||||||
'name': manifest.get('name')
|
'name': manifest.get('name')
|
||||||
}
|
}
|
||||||
except Exception:
|
except Exception: # nosec B110 - corrupt/unreadable manifest; skip this plugin, outer except logs
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.warning(f"Error reading disk state: {e}")
|
self.logger.warning(f"Error reading disk state: {e}")
|
||||||
|
|||||||
@@ -689,7 +689,7 @@ class WiFiManager:
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
_IP_FORWARD_SAVE_PATH = Path("/tmp/ledmatrix_ip_forward_saved")
|
_IP_FORWARD_SAVE_PATH = Path("/tmp/ledmatrix_ip_forward_saved") # nosec B108 - process-specific named file; device is single-user RPi
|
||||||
|
|
||||||
def _validate_ap_config(self) -> Tuple[str, int]:
|
def _validate_ap_config(self) -> Tuple[str, int]:
|
||||||
"""Return a sanitized (ssid, channel) pair from config, falling back to defaults."""
|
"""Return a sanitized (ssid, channel) pair from config, falling back to defaults."""
|
||||||
@@ -890,14 +890,14 @@ class WiFiManager:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
content = f"# LEDMatrix captive portal: resolve all hostnames to AP\naddress=/#/{ap_ip}\n"
|
content = f"# LEDMatrix captive portal: resolve all hostnames to AP\naddress=/#/{ap_ip}\n"
|
||||||
with open("/tmp/ledmatrix-nm-dnsmasq.conf", "w") as f:
|
with open("/tmp/ledmatrix-nm-dnsmasq.conf", "w") as f: # nosec B108 - named file matches sudoers allowlist; single-user device
|
||||||
f.write(content)
|
f.write(content)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["sudo", "mkdir", "-p", str(NM_DNSMASQ_SHARED_DIR)],
|
["sudo", "mkdir", "-p", str(NM_DNSMASQ_SHARED_DIR)],
|
||||||
capture_output=True, timeout=5
|
capture_output=True, timeout=5
|
||||||
)
|
)
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["sudo", "cp", "/tmp/ledmatrix-nm-dnsmasq.conf", str(NM_DNSMASQ_SHARED_CONF)],
|
["sudo", "cp", "/tmp/ledmatrix-nm-dnsmasq.conf", str(NM_DNSMASQ_SHARED_CONF)], # nosec B108
|
||||||
capture_output=True, timeout=5
|
capture_output=True, timeout=5
|
||||||
)
|
)
|
||||||
logger.info(f"Wrote NM dnsmasq captive-portal config: {NM_DNSMASQ_SHARED_CONF}")
|
logger.info(f"Wrote NM dnsmasq captive-portal config: {NM_DNSMASQ_SHARED_CONF}")
|
||||||
@@ -1401,7 +1401,7 @@ class WiFiManager:
|
|||||||
# Last resort: enable AP mode
|
# Last resort: enable AP mode
|
||||||
try:
|
try:
|
||||||
self.enable_ap_mode()
|
self.enable_ap_mode()
|
||||||
except Exception:
|
except Exception: # nosec B110 - last-resort recovery; if AP enable fails there's nothing left to try
|
||||||
pass
|
pass
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
@@ -2324,12 +2324,12 @@ ignore_broadcast_ssid=0
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Write config (requires sudo)
|
# Write config (requires sudo)
|
||||||
with open("/tmp/hostapd.conf", 'w') as f:
|
with open("/tmp/hostapd.conf", 'w') as f: # nosec B108 - named file matches sudoers allowlist; single-user device
|
||||||
f.write(config_content)
|
f.write(config_content)
|
||||||
|
|
||||||
# Copy to final location with sudo
|
# Copy to final location with sudo
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["sudo", "cp", "/tmp/hostapd.conf", str(HOSTAPD_CONFIG_PATH)],
|
["sudo", "cp", "/tmp/hostapd.conf", str(HOSTAPD_CONFIG_PATH)], # nosec B108
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2394,12 +2394,12 @@ address=/detectportal.firefox.com/192.168.4.1
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Write config (requires sudo)
|
# Write config (requires sudo)
|
||||||
with open("/tmp/dnsmasq.conf", 'w') as f:
|
with open("/tmp/dnsmasq.conf", 'w') as f: # nosec B108 - named file matches sudoers allowlist; single-user device
|
||||||
f.write(config_content)
|
f.write(config_content)
|
||||||
|
|
||||||
# Copy to final location with sudo
|
# Copy to final location with sudo
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["sudo", "cp", "/tmp/dnsmasq.conf", str(DNSMASQ_CONFIG_PATH)],
|
["sudo", "cp", "/tmp/dnsmasq.conf", str(DNSMASQ_CONFIG_PATH)], # nosec B108
|
||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ def after_request_logging(response):
|
|||||||
duration_ms=duration_ms,
|
duration_ms=duration_ms,
|
||||||
ip_address=ip_address
|
ip_address=ip_address
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception: # nosec B110 - request logging must never interrupt a live HTTP response
|
||||||
pass # Don't break response if logging fails
|
pass # Don't break response if logging fails
|
||||||
return response
|
return response
|
||||||
|
|
||||||
@@ -502,7 +502,7 @@ def display_preview_generator():
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
import io
|
import io
|
||||||
|
|
||||||
snapshot_path = "/tmp/led_matrix_preview.png"
|
snapshot_path = "/tmp/led_matrix_preview.png" # nosec B108 - fixed path matches display_manager; only read here
|
||||||
last_modified = None
|
last_modified = None
|
||||||
|
|
||||||
# Get display dimensions from config
|
# Get display dimensions from config
|
||||||
@@ -542,7 +542,7 @@ def display_preview_generator():
|
|||||||
}
|
}
|
||||||
last_modified = current_modified
|
last_modified = current_modified
|
||||||
yield preview_data
|
yield preview_data
|
||||||
except Exception:
|
except Exception: # nosec B110 - SSE preview file may be mid-write; transient error, skip this update
|
||||||
# File might be being written, skip this update
|
# File might be being written, skip this update
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ def _load_plugin_config_partial(plugin_id):
|
|||||||
config['images'] = config.get('images', []) + new_images
|
config['images'] = config.get('images', []) + new_images
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Could not load metadata for {plugin_id}: {e}")
|
print(f"Warning: Could not load metadata for {plugin_id}: {e}")
|
||||||
except Exception:
|
except Exception: # nosec B110 - metadata pre-load is optional; schema loads fully below
|
||||||
pass # Will load schema properly below
|
pass # Will load schema properly below
|
||||||
|
|
||||||
# Get plugin schema
|
# Get plugin schema
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def get_local_ips():
|
|||||||
)
|
)
|
||||||
if result.returncode == 0 and result.stdout.strip() == "active":
|
if result.returncode == 0 and result.stdout.strip() == "active":
|
||||||
ips.append("192.168.4.1 (AP Mode)")
|
ips.append("192.168.4.1 (AP Mode)")
|
||||||
except Exception:
|
except Exception: # nosec B110 - AP mode IP detection is non-critical startup info; systemctl may not exist
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Get IPs from hostname -I
|
# Get IPs from hostname -I
|
||||||
|
|||||||
Reference in New Issue
Block a user