From 5ce469b96e760ab89d138b33d390b7e254c527bb Mon Sep 17 00:00:00 2001 From: cmw-creator Date: Thu, 4 Jun 2026 03:56:36 +0800 Subject: [PATCH] docs: enhance code comments and docstrings across the codebase - Add module-level docstrings to all core packages - Enhance function docstrings with parameter details and return values - Add inline comments for complex logic (cache, mirror selection, vulnerability matching) - Document the event-bus pattern, auth subsystem, and tool interception lifecycle Closes #576 --- safety/__init__.py | 24 +++++++++++++++ safety/auth/__init__.py | 15 +++++++++ safety/auth/constants.py | 13 ++++++++ safety/auth/main.py | 15 +++++++++ safety/cli.py | 7 +++++ safety/cli_util.py | 7 +++++ safety/config/__init__.py | 12 ++++++++ safety/constants.py | 12 ++++++++ safety/events/__init__.py | 18 +++++++++++ safety/formatter.py | 14 +++++++++ safety/models/__init__.py | 10 ++++++ safety/safety.py | 65 ++++++++++++++++++++++++++++++++------- safety/tool/__init__.py | 33 +++++++++++++++++++- safety/util.py | 23 +++++++++++--- 14 files changed, 252 insertions(+), 16 deletions(-) diff --git a/safety/__init__.py b/safety/__init__.py index d48113e2..0a7cf071 100644 --- a/safety/__init__.py +++ b/safety/__init__.py @@ -1,4 +1,28 @@ # -*- coding: utf-8 -*- +""" +Safety CLI — Vulnerability scanning for Python (and other ecosystem) dependencies. + +This package is the core entry point for the Safety CLI tool. It provides +vulnerability scanning, policy enforcement, authentication, event tracking, +and tool-interception (firewall) capabilities for Python and npm ecosystems. + +Top-level modules: + - cli: Click/Typer command-line interface definitions + - safety: Core vulnerability database fetching, caching, and checking + - auth: Authentication (OAuth2, API keys, machine enrollment) + - scan: Modern scan command (project & system scans) + - tool: Package-tool interception (pip, poetry, uv, npm) + - firewall: Package firewall for proxy-based security + - config: Configuration management + - events: Telemetry and security-event emission + - models: Data models (Vulnerability, Package, etc.) + - formatters: Report output formatting (screen, json, html, bare, text) + +Sub-packages: + - alerts: Deprecated alert/notification system + - codebase: Codebase initialization and management + - init: Safety init workflow (project setup) +""" __author__ = """safetycli.com""" __email__ = "cli@safetycli.com" diff --git a/safety/auth/__init__.py b/safety/auth/__init__.py index fc14e290..24081e5d 100644 --- a/safety/auth/__init__.py +++ b/safety/auth/__init__.py @@ -1,3 +1,18 @@ +""" +Authentication subsystem for the Safety CLI. + +Provides OAuth2-based login/logout via Auth0, machine-token enrollment +for MDM scenarios, and session management. The main entry points are: + + - ``cli`` — Typer sub-command (``safety auth login | logout | status``) + - ``cli_utils`` — Click option decorators (``--key``, proxy settings) + - ``main`` — Core auth flow orchestration (login, logout, callback handling) + - ``oauth2`` — OAuth2 client wrapper (token refresh, revocation) + - ``models`` — Auth/OAuth data models (Token, Organization, Auth) + - ``server`` — Local HTTP server for OAuth2 callback redirect + - ``enrollment`` — Machine enrollment flow (MDM) +""" + from .cli_utils import auth_options, proxy_options, configure_auth_session from .cli import auth diff --git a/safety/auth/constants.py b/safety/auth/constants.py index b5053d26..19ac5d01 100644 --- a/safety/auth/constants.py +++ b/safety/auth/constants.py @@ -1,3 +1,12 @@ +""" +Authentication-specific constants and URLs. + +Defines the OAuth2 endpoints, scopes, claim URLs, and CLI messaging +templates used by the authentication subsystem. All URLs are resolved +through ``safety.constants.get_required_config_setting`` to support +runtime overrides via environment variables or config files. +""" + from safety.constants import get_required_config_setting HOST: str = "localhost" @@ -5,6 +14,7 @@ CLIENT_ID = get_required_config_setting("CLIENT_ID") AUTH_SERVER_URL = get_required_config_setting("AUTH_SERVER_URL") SAFETY_PLATFORM_URL = get_required_config_setting("SAFETY_PLATFORM_URL") +# OpenID Connect scope requesting identity, email, and offline refresh token OAUTH2_SCOPE = "openid email profile offline_access" @@ -13,12 +23,14 @@ CLAIM_EMAIL_VERIFIED_API = "https://api.safetycli.com/email_verified" CLAIM_EMAIL_VERIFIED_AUTH_SERVER = "email_verified" +# CLI-specific auth endpoints on the Safety Platform CLI_AUTH = f"{SAFETY_PLATFORM_URL}/cli/auth" CLI_AUTH_SUCCESS = f"{SAFETY_PLATFORM_URL}/cli/auth/success" CLI_AUTH_LOGOUT = f"{SAFETY_PLATFORM_URL}/cli/logout" CLI_CALLBACK = f"{SAFETY_PLATFORM_URL}/cli/callback" CLI_LOGOUT_SUCCESS = f"{SAFETY_PLATFORM_URL}/cli/logout/success" +# Messaging templates MSG_NON_AUTHENTICATED = ( "Safety is not authenticated. Please run 'safety auth login' to log in" " or 'safety auth enroll' to enroll via MDM." @@ -32,6 +44,7 @@ MSG_LOGOUT_FAILED = "[red]Logout failed. Try again.[/red]" ENROLLMENT_ENDPOINT = "/api/enroll" +# Pattern for Safety Enrollment Keys (sfek_ prefix, 43 alphanumeric chars + hyphens/underscores) ENROLLMENT_KEY_PATTERN = r"^sfek_[A-Za-z0-9_-]{43}$" MACHINE_ID_MAX_LENGTH = 255 MSG_MACHINE_TOKEN_NOT_ACCEPTED = "Machine token authentication is not accepted for this operation. Run 'safety auth login' or use '--key' to authenticate." diff --git a/safety/auth/main.py b/safety/auth/main.py index 0c3c6c12..24abc1b8 100644 --- a/safety/auth/main.py +++ b/safety/auth/main.py @@ -1,3 +1,18 @@ +""" +Core authentication workflow for the Safety CLI. + +Handles the OAuth2 authorization code flow with PKCE for user login, +session token management, token refresh, and logout. Supports: + + - Interactive login via browser (opens auth.safetycli.com) + - Headless login with manual URL copy-paste + - Machine enrollment (MDM) with pre-provisioned tokens + - API-key-based authentication for CI/CD environments + +The flow uses Authlib's ``OAuth2Client`` and stores tokens in +``~/.safety/auth.ini``. +""" + import configparser from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union diff --git a/safety/cli.py b/safety/cli.py index 14ec46ae..0ea32d20 100644 --- a/safety/cli.py +++ b/safety/cli.py @@ -203,6 +203,12 @@ def cli(ctx, debug, disable_optional_telemetry): def clean_check_command(f): """ Main entry point for validation. + + This decorator wraps the legacy ``check`` command to handle: + - Authentication validation (--apply-security-updates requires auth) + - Policy file loading for auto-remediation limits + - Mutually exclusive option validation (--json-version) + - Legacy key/proxy argument cleanup """ @wraps(f) @@ -548,6 +554,7 @@ def check( """ LOG.info("Running check command") + # Determine if we're in non-interactive mode (CI/CD, pipe, etc.) non_interactive = ( not sys.stdout.isatty() and os.environ.get("SAFETY_OS_DESCRIPTION", None) != "run" diff --git a/safety/cli_util.py b/safety/cli_util.py index 9ac8cbfb..4e5ab7ed 100644 --- a/safety/cli_util.py +++ b/safety/cli_util.py @@ -58,6 +58,13 @@ class CommandType(Enum): + """ + Classifies CLI commands for help-text rendering groups. + + - MAIN: Primary feature commands (scan, check, auth) + - UTILITY: Ancillary utilities (generate, configure) + - BETA: Experimental features flagged with a beta disclaimer + """ MAIN = "main" UTILITY = "utility" BETA = "beta" diff --git a/safety/config/__init__.py b/safety/config/__init__.py index b8ca378d..7dd178f0 100644 --- a/safety/config/__init__.py +++ b/safety/config/__init__.py @@ -1,3 +1,15 @@ +""" +Configuration management for Safety CLI. + +Manages authentication config (``auth.ini``), proxy settings, and TLS +configuration. Config files are stored in ``~/.safety/`` (user) and +``/etc/.safety/`` (system). + + - ``auth`` — AuthConfig model (OAuth tokens, API keys, machine credentials) + - ``proxy`` — Proxy configuration resolution (HTTP/HTTPS proxies) + - ``tls`` — TLS/SSL certificate configuration +""" + from .auth import AuthConfig, MachineCredentialConfig from .proxy import get_proxy_config from .tls import get_tls_config diff --git a/safety/constants.py b/safety/constants.py index 6166ad3d..49c2ee1a 100644 --- a/safety/constants.py +++ b/safety/constants.py @@ -1,4 +1,14 @@ # -*- coding: utf-8 -*- +""" +Constants, configuration paths, and URL settings for the Safety CLI. + +This module defines: + - Filesystem paths for config/cache/policy files (user and system level) + - URL endpoints for the Safety Platform, Auth, Data, and Firewall APIs + - Exit codes used by the CLI for script-friendly error handling + - Feature flags and messaging templates used throughout the application + - ANSI color name constants reused across the formatters +""" import configparser import os from enum import Enum @@ -10,6 +20,8 @@ JSON_SCHEMA_VERSION = "2.0.0" # TODO fix this +# Free/open vulnerability database mirrors — used when no API key is provided. +# These serve a limited subset of the full vulnerability database. OPEN_MIRRORS = [ f"https://pyup.io/aws/safety/free/{JSON_SCHEMA_VERSION}/", ] diff --git a/safety/events/__init__.py b/safety/events/__init__.py index dbeda82d..89480fe1 100644 --- a/safety/events/__init__.py +++ b/safety/events/__init__.py @@ -1,3 +1,21 @@ +""" +Event emission and handling subsystem. + +This package is responsible for emitting, routing, and handling telemetry +and security events throughout the Safety CLI. It implements a lightweight +event-bus pattern: + + - ``event_bus/`` — The core event bus (pub/sub) and utilities + - ``handlers/`` — Event handler implementations (sending to platform) + - ``types/`` — Event type definitions (internal + external payloads) + - ``utils/`` — Event creation, emission helpers, and conditions + +Events are used for: + - Telemetry (CLI usage metrics sent to Safety Platform) + - Security traces (firewall blocks, package installs, diffs) + - Internal lifecycle (flush, close resources) +""" + from .handlers import EventHandler from .types import ( CloseResourcesEvent, diff --git a/safety/formatter.py b/safety/formatter.py index d4d91fea..042b64ef 100644 --- a/safety/formatter.py +++ b/safety/formatter.py @@ -1,3 +1,17 @@ +""" +Output formatting abstraction for Safety CLI reports. + +Uses the **Strategy** pattern: ``SafetyFormatter`` is the context that +delegates to one of several concrete format implementations: + + - ``ScreenReport`` — Rich-formatted terminal output (default) + - ``TextReport`` — Plain text stripped of Rich markup + - ``JsonReport`` — Structured JSON output + - ``BareReport`` — Minimal machine-readable output + - ``HTMLReport`` — Self-contained HTML report for sharing + +All implementations conform to the ``FormatterAPI`` protocol. +""" from __future__ import annotations import logging diff --git a/safety/models/__init__.py b/safety/models/__init__.py index 79765de6..b008bc94 100644 --- a/safety/models/__init__.py +++ b/safety/models/__init__.py @@ -1,3 +1,13 @@ +""" +Package data models for the Safety CLI. + +This module re-exports the main model classes used throughout Safety: + - Vulnerability / CVE / Severity / Fix — vulnerability report data + - SafetyRequirement / Package — parsed dependency data + - RequirementFile — parsed requirements files + - SafetyCLI — CLI context object + - ToolResult — result from tool interception +""" from .obj import SafetyCLI from .requirements import is_pinned_requirement from .vulnerabilities import ( diff --git a/safety/safety.py b/safety/safety.py index a86c1679..ead95fb7 100644 --- a/safety/safety.py +++ b/safety/safety.py @@ -1,5 +1,23 @@ # -*- coding: utf-8 -*- # type: ignore +""" +Core vulnerability database operations and package scanning logic. + +This module is the engine behind the legacy ``safety check`` command. It +handles: + + - Fetching vulnerability databases from remote mirrors (API or free tier) + - Reading local vulnerability databases from disk + - Caching database responses with configurable TTL + - Matching installed/required packages against known vulnerabilities + - Computing remediations (recommended version bumps) + - Applying automatic security updates to requirement files + +Key functions: + fetch_database — resolve mirror (URL or local path) and fetch DB + check — scan packages against a loaded DB + process_fixes_scan — apply remediations and write fixed requirement files +""" from dataclasses import asdict import errno import itertools @@ -284,7 +302,10 @@ def fetch_database_url( except httpx.TimeoutException: raise RequestTimeoutError() except httpx.RequestError: - raise DatabaseFetchError() + raise DatabaseFetchError( + "Unable to fetch the vulnerability database due to an unexpected HTTP error. " + "Please check your network connection and try again." + ) except OAuthError as e: LOG.error("OAuthError: %s", e) raise InvalidCredentialError() @@ -375,21 +396,31 @@ def fetch_database( """ Fetches the database from a mirror or a local file. + This is the main entry point for database resolution. It determines the + correct source based on authentication state: + - Authenticated users → API_MIRRORS (private/paid vulnerability feed) + - ``--db`` argument → local path or custom remote mirror + - Neither → OPEN_MIRRORS (free, limited feed) + Args: - http_client (OAuth2Client): The OAuth2 client. - platform (SafetyPlatformClient): The Safety Platform client. - full (bool): Whether to fetch the full database. - db (Optional[str]): The path to the local database file. - cached (int): The cache validity in seconds. + auth (Auth): The Auth model holding platform client credentials. + full (bool): Whether to fetch the full database (``insecure_full.json``). + db (Optional[str]): The path to the local database file or a custom mirror URL. + cached (int): The cache validity in seconds (0 = no caching). telemetry (bool): Whether to include telemetry data. - ecosystem (Optional[Ecosystem]): The ecosystem. - from_cache (bool): Whether to fetch from cache. + ecosystem (Optional[Ecosystem]): The ecosystem filter. + from_cache (bool): Whether to attempt a cache hit before fetching. Returns: Dict[str, Any]: The fetched database. + + Raises: + DatabaseFetchError: If the database cannot be fetched from any source. + MalformedDatabase: If the fetched data has an unsupported schema version. """ from safety.utils.auth_session import AuthenticationType + # Machine tokens (used for MDM enrollment) are not valid for database fetching. if auth.platform.get_authentication_type() == AuthenticationType.machine_token: raise DatabaseFetchError( "Machine token authentication is not accepted for this operation. " @@ -434,7 +465,10 @@ def fetch_database( f"This Safety version supports only schema version {JSON_SCHEMA_VERSION}", ) - raise DatabaseFetchError() + raise DatabaseFetchError( + "Unable to fetch the vulnerability database from any of the configured sources. " + "Please check your network connection, API key, and database mirror configuration." + ) def get_vulnerabilities( @@ -656,6 +690,11 @@ def is_vulnerable( """ Checks if a package version is vulnerable. + For pinned packages (e.g. ``Django==3.2``) this uses a simple ``contains()`` + check on the vulnerable spec. For unpinned ranges (e.g. ``Django>=3.0,<4.0``) + it intersects the install specifier with the vulnerable specifier and returns + ``True`` if any version falls in both ranges. + Args: vulnerable_spec (SpecifierSet): The specifier set for vulnerable versions. requirement (SafetyRequirement): The package requirement. @@ -668,7 +707,8 @@ def is_vulnerable( try: return vulnerable_spec.contains(next(iter(requirement.specifier)).version) except Exception: - # Ugly for now... + # If the version string is invalid per PEP 440, skip silently + # but warn the user via local announcements. message = f"Version {requirement.specifier} for {package.name} is invalid and is ignored by Safety. Please See PEP 440." if message not in [a["message"] for a in SafetyContext.local_announcements]: SafetyContext.local_announcements.append( @@ -1732,7 +1772,10 @@ def get_licenses( licenses = fetch_database_file(mirror, db_name=db_name, ecosystem=None) if licenses: return licenses - raise DatabaseFetchError() + raise DatabaseFetchError( + "Unable to fetch the vulnerability database from any of the configured sources. " + "Please check your network connection, API key, and database mirror configuration." + ) def add_local_notifications( diff --git a/safety/tool/__init__.py b/safety/tool/__init__.py index 70bf6c2d..a966d03f 100644 --- a/safety/tool/__init__.py +++ b/safety/tool/__init__.py @@ -1,5 +1,36 @@ +""" +Package tool interception (firewall) subsystem. + +This package intercepts package-management commands (pip install, poetry add, +uv add, npm install, etc.) so Safety can: + + 1. **Verify** that packages being installed are not known to be vulnerable + or malicious (typosquatting protection). + 2. **Track** environment changes (package additions, removals, updates) + for audit / rollback support. + 3. **Emit** security events to the Safety Platform for monitoring. + +Architecture +------------ +Each supported tool has its own sub-package (``pip/``, ``poetry/``, ``uv/``, +``npm/``) containing: + + - ``command.py`` — CLI command definition (Typer) + - ``parser.py`` — Command-line parser (ToolCommandLineParser subclass) + - ``main.py`` — ToolCommandBase subclass implementing the interception + +The interception lifecycle is: + + ``before()`` → Typosquatting check & diff tracking setup + ``execute()`` → Run the real tool via subprocess + ``after()`` → Diff computation & event emission + +The ``interceptors/`` sub-package provides OS-level process interception +(unix, windows) for the firewall mode. +""" + from .tool_inspector import ToolInspector -from .factory import tool_commands +from .main import tool_commands from .main import configure_system, configure_alias from .base import ToolResult diff --git a/safety/util.py b/safety/util.py index e4e3ff97..bad52e2c 100644 --- a/safety/util.py +++ b/safety/util.py @@ -48,7 +48,7 @@ def is_a_remote_mirror(mirror: str) -> bool: """ - Check if a mirror URL is remote. + Check if a mirror URL is remote (http/https) vs local filesystem path. Args: mirror (str): The mirror URL. @@ -63,6 +63,9 @@ def is_supported_by_parser(path: str) -> bool: """ Check if the file path is supported by the parser. + Safety can parse requirements.txt, Pipfile, Pipfile.lock, setup.cfg, + poetry.lock, and various other dependency file formats. + Args: path (str): The file path. @@ -1186,10 +1189,14 @@ def shell_complete( class SingletonMeta(type): """ - A metaclass for singleton classes. + Thread-safe singleton metaclass. + + Ensures that only one instance of any class using this metaclass exists + at runtime. Uses a class-level lock to prevent race conditions during + concurrent instantiation. """ - _instances: Dict[type, Any] = {} + _instances: Dict[type, object] = {} _lock: Lock = Lock() @@ -1203,7 +1210,15 @@ def __call__(cls, *args: Any, **kwargs: Any) -> Any: class SafetyContext(metaclass=SingletonMeta): """ - A singleton class to hold the Safety context. + Global singleton holding the Safety CLI execution context. + + This is the single source of truth for the current scan session's + state, including packages, authentication, database mirror, ignored + vulnerabilities, proxy settings, and telemetry configuration. + + It is populated by the ``@sync_safety_context`` decorator before each + major command handler runs, and read by formatters, reporters, and + event emitters throughout the lifecycle of a command. """ packages = []