Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions safety/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
15 changes: 15 additions & 0 deletions safety/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
13 changes: 13 additions & 0 deletions safety/auth/constants.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
"""
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"

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"


Expand All @@ -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."
Expand All @@ -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."
15 changes: 15 additions & 0 deletions safety/auth/main.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 7 additions & 0 deletions safety/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions safety/cli_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
12 changes: 12 additions & 0 deletions safety/config/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 12 additions & 0 deletions safety/constants.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}/",
]
Expand Down
18 changes: 18 additions & 0 deletions safety/events/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
14 changes: 14 additions & 0 deletions safety/formatter.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 10 additions & 0 deletions safety/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
65 changes: 54 additions & 11 deletions safety/safety.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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. "
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
33 changes: 32 additions & 1 deletion safety/tool/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading