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:
Chuck
2026-05-14 13:05:14 -04:00
parent 4d2a567597
commit 3aaf156962
10 changed files with 22 additions and 22 deletions

View File

@@ -81,7 +81,7 @@ def main() -> int:
help='Plugin config as JSON string')
parser.add_argument('--mock-data', '-m', default=None,
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)')
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)')

View File

@@ -172,7 +172,7 @@ class SportsCore(ABC):
try:
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
fallbacks.append(Path(tempfile.gettempdir()) / "ledmatrix_logos" / self.sport_key)

View File

@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
# System directories that should never have their permissions modified
# 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',
'/var/tmp',
'/dev',

View File

@@ -813,8 +813,8 @@ class DisplayController:
# 1. Explicit hook — plugin opted in with get_offset_frame()
try:
follower_frame = plugin_instance.get_offset_frame(offset)
except Exception:
pass
except AttributeError:
pass # Most plugins don't implement get_offset_frame; that's expected
# 2. Auto-detect — plugin has a scroll_helper (standard pattern for all
# scroll plugins). Works with zero plugin code changes.

View File

@@ -32,7 +32,7 @@ class DisplayManager:
# When True, update_display() and clear() skip hardware writes (used during off-screen content capture)
self._capture_mode_active = False
# 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._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.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))
except Exception:
except Exception: # nosec B110 - best-effort fallback visualization; drawing errors must not crash startup
# Best-effort; ignore drawing errors in fallback
pass
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)
# that must not be changed or it breaks apt and other system tools
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())
# Write atomically: temp then replace
tmp_path = f"{self._snapshot_path}.tmp"

View File

@@ -234,7 +234,7 @@ class StateReconciliation:
'version': manifest.get('version'),
'name': manifest.get('name')
}
except Exception:
except Exception: # nosec B110 - corrupt/unreadable manifest; skip this plugin, outer except logs
pass
except Exception as e:
self.logger.warning(f"Error reading disk state: {e}")

View File

@@ -689,7 +689,7 @@ class WiFiManager:
# 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]:
"""Return a sanitized (ssid, channel) pair from config, falling back to defaults."""
@@ -890,14 +890,14 @@ class WiFiManager:
"""
try:
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)
subprocess.run(
["sudo", "mkdir", "-p", str(NM_DNSMASQ_SHARED_DIR)],
capture_output=True, timeout=5
)
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
)
logger.info(f"Wrote NM dnsmasq captive-portal config: {NM_DNSMASQ_SHARED_CONF}")
@@ -1401,7 +1401,7 @@ class WiFiManager:
# Last resort: enable AP mode
try:
self.enable_ap_mode()
except Exception:
except Exception: # nosec B110 - last-resort recovery; if AP enable fails there's nothing left to try
pass
return False, str(e)
@@ -2324,12 +2324,12 @@ ignore_broadcast_ssid=0
"""
# 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)
# Copy to final location with sudo
subprocess.run(
["sudo", "cp", "/tmp/hostapd.conf", str(HOSTAPD_CONFIG_PATH)],
["sudo", "cp", "/tmp/hostapd.conf", str(HOSTAPD_CONFIG_PATH)], # nosec B108
timeout=10
)
@@ -2394,12 +2394,12 @@ address=/detectportal.firefox.com/192.168.4.1
"""
# 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)
# Copy to final location with sudo
subprocess.run(
["sudo", "cp", "/tmp/dnsmasq.conf", str(DNSMASQ_CONFIG_PATH)],
["sudo", "cp", "/tmp/dnsmasq.conf", str(DNSMASQ_CONFIG_PATH)], # nosec B108
timeout=10
)

View File

@@ -324,7 +324,7 @@ def after_request_logging(response):
duration_ms=duration_ms,
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
return response
@@ -502,7 +502,7 @@ def display_preview_generator():
from PIL import Image
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
# Get display dimensions from config
@@ -542,7 +542,7 @@ def display_preview_generator():
}
last_modified = current_modified
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
pass
else:

View File

@@ -395,7 +395,7 @@ def _load_plugin_config_partial(plugin_id):
config['images'] = config.get('images', []) + new_images
except Exception as 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
# Get plugin schema

View File

@@ -25,7 +25,7 @@ def get_local_ips():
)
if result.returncode == 0 and result.stdout.strip() == "active":
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
# Get IPs from hostname -I