diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 0cded2c..ac31272 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -83,10 +83,8 @@ jobs: pip-compile --generate-hashes --output-file=requirements.lock src/requirements.txt git diff --exit-code requirements.lock pip install --require-hashes -r requirements.lock - - name: Run pip-audit - run: | - pip install pip-audit - pip-audit -r requirements.lock --ignore-vuln GHSA-wj6h-64fc-37mp + - name: Run dependency scan + run: scripts/dependency_scan.sh --ignore-vuln GHSA-wj6h-64fc-37mp - name: Determine stress args shell: bash run: | diff --git a/scripts/dependency_scan.sh b/scripts/dependency_scan.sh new file mode 100755 index 0000000..bdb4589 --- /dev/null +++ b/scripts/dependency_scan.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Run pip-audit against the pinned requirements +if ! command -v pip-audit >/dev/null 2>&1; then + python -m pip install --quiet pip-audit +fi + +pip-audit -r requirements.lock "$@" diff --git a/src/main.py b/src/main.py index c1b378f..21f3271 100644 --- a/src/main.py +++ b/src/main.py @@ -18,6 +18,7 @@ from colorama import init as colorama_init from termcolor import colored from utils.color_scheme import color_text import traceback +import importlib from seedpass.core.manager import PasswordManager from nostr.client import NostrClient @@ -38,6 +39,25 @@ from local_bip85.bip85 import Bip85Error colorama_init() +OPTIONAL_DEPENDENCIES = { + "pyperclip": "clipboard support for secret mode", + "qrcode": "QR code generation for TOTP setup", + "toga": "desktop GUI features", +} + + +def _warn_missing_optional_dependencies() -> None: + """Log warnings for any optional packages that are not installed.""" + for module, feature in OPTIONAL_DEPENDENCIES.items(): + try: + importlib.import_module(module) + except ModuleNotFoundError: + logging.warning( + "Optional dependency '%s' is not installed; %s will be unavailable.", + module, + feature, + ) + def load_global_config() -> dict: """Load configuration from ~/.seedpass/config.toml if present.""" @@ -1205,6 +1225,7 @@ def main(argv: list[str] | None = None, *, fingerprint: str | None = None) -> in Optional seed profile fingerprint to select automatically. """ configure_logging() + _warn_missing_optional_dependencies() initialize_app() logger = logging.getLogger(__name__) logger.info("Starting SeedPass Password Manager") diff --git a/src/requirements.txt b/src/requirements.txt index 59ef524..af1ef30 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -22,19 +22,21 @@ pgpy==0.6.0 pyotp>=2.8.0 freezegun -pyperclip -qrcode>=8.2 typer>=0.12.3 -fastapi>=0.116.1 -uvicorn>=0.35.0 -starlette>=0.47.2 -httpx>=0.28.1 -requests>=2.32 -python-multipart>=0.0.20 -PyJWT -orjson -argon2-cffi -toga-core>=0.5.2 -pillow -toga-dummy>=0.5.2 # for headless GUI tests -slowapi + +# Optional dependencies - install as needed for additional features +pyperclip # Clipboard support for secret mode +qrcode>=8.2 # Generate QR codes for TOTP setup +fastapi>=0.116.1 # API server +uvicorn>=0.35.0 # API server +starlette>=0.47.2 # API server +httpx>=0.28.1 # API server +requests>=2.32 # API server +python-multipart>=0.0.20 # API server file uploads +PyJWT # JWT authentication for API server +orjson # Fast JSON serialization for API server +argon2-cffi # Password hashing for API server +toga-core>=0.5.2 # Desktop GUI +pillow # Image support for GUI +toga-dummy>=0.5.2 # Headless GUI tests +slowapi # Rate limiting for API server diff --git a/src/utils/__init__.py b/src/utils/__init__.py index f9b8ad4..0061f7f 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,47 +1,51 @@ # utils/__init__.py +"""Utility package exports and optional feature handling.""" + import logging -import traceback logger = logging.getLogger(__name__) -try: - from .file_lock import exclusive_lock, shared_lock - from .key_derivation import ( - derive_key_from_password, - derive_key_from_parent_seed, - derive_index_key, - derive_totp_secret, - EncryptionMode, - DEFAULT_ENCRYPTION_MODE, - TOTP_PURPOSE, - ) - from .checksum import ( - calculate_checksum, - verify_checksum, - json_checksum, - canonical_json_dumps, - initialize_checksum, - update_checksum_file, - ) - from .password_prompt import prompt_for_password - from .seed_prompt import masked_input, prompt_seed_words - from .input_utils import timed_input - from .memory_protection import InMemorySecret - from .clipboard import copy_to_clipboard - from .terminal_utils import ( - clear_screen, - pause, - clear_and_print_fingerprint, - clear_header_with_notification, - ) - from .atomic_write import atomic_write +from .file_lock import exclusive_lock, shared_lock +from .key_derivation import ( + derive_key_from_password, + derive_key_from_parent_seed, + derive_index_key, + derive_totp_secret, + EncryptionMode, + DEFAULT_ENCRYPTION_MODE, + TOTP_PURPOSE, +) +from .checksum import ( + calculate_checksum, + verify_checksum, + json_checksum, + canonical_json_dumps, + initialize_checksum, + update_checksum_file, +) +from .password_prompt import prompt_for_password +from .seed_prompt import masked_input, prompt_seed_words +from .input_utils import timed_input +from .memory_protection import InMemorySecret +from .terminal_utils import ( + clear_screen, + pause, + clear_and_print_fingerprint, + clear_header_with_notification, +) +from .atomic_write import atomic_write + +# Optional clipboard support +try: # pragma: no cover - exercised when dependency missing + from .clipboard import copy_to_clipboard +except Exception as exc: # pragma: no cover - executed only if pyperclip missing + + def copy_to_clipboard(*_args, **_kwargs): + """Stub when clipboard support is unavailable.""" + logger.warning("Clipboard support unavailable: %s", exc) + return False - if logger.isEnabledFor(logging.DEBUG): - logger.info("Modules imported successfully.") -except Exception as e: - if logger.isEnabledFor(logging.DEBUG): - logger.error(f"Failed to import one or more modules: {e}", exc_info=True) __all__ = [ "derive_key_from_password",