This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

View File

@@ -0,0 +1,210 @@
import logging
import sys
from enum import Enum
import typer
from rich.prompt import Prompt
from safety.console import main_console as console
from safety.decorators import notify
from safety.events.utils import emit_firewall_disabled
from typing import List, Optional
# TODO: refactor this import and the related code
# For now, let's keep it as is
from safety.error_handlers import handle_cmd_exception
from ..cli_util import (
CommandType,
FeatureType,
SafetyCLICommand,
SafetyCLISubGroup,
pass_safety_cli_obj,
)
from ..constants import (
CONTEXT_COMMAND_TYPE,
CONTEXT_FEATURE_TYPE,
EXIT_CODE_OK,
DEFAULT_EPILOG,
)
from ..tool.interceptors import create_interceptor
from ..tool.main import reset_system
from .constants import (
FIREWALL_CMD_NAME,
FIREWALL_HELP,
MSG_FEEDBACK,
MSG_REQ_FILE_LINE,
MSG_UNINSTALL_EXPLANATION,
MSG_UNINSTALL_WRAPPERS,
MSG_UNINSTALL_CONFIG,
MSG_UNINSTALL_SUCCESS,
UNINSTALL_CMD_NAME,
UNINSTALL_HELP,
INIT_CMD_NAME,
INIT_HELP,
MSG_INIT_SUCCESS,
)
firewall_app = typer.Typer(
rich_markup_mode="rich", cls=SafetyCLISubGroup, name=FIREWALL_CMD_NAME
)
LOG = logging.getLogger(__name__)
init_app = typer.Typer(rich_markup_mode="rich", cls=SafetyCLISubGroup)
@firewall_app.callback(
cls=SafetyCLISubGroup,
help=FIREWALL_HELP,
epilog=DEFAULT_EPILOG,
context_settings={
"allow_extra_args": True,
"ignore_unknown_options": True,
CONTEXT_COMMAND_TYPE: CommandType.BETA,
CONTEXT_FEATURE_TYPE: FeatureType.FIREWALL,
},
)
@pass_safety_cli_obj
def firewall(ctx: typer.Context) -> None:
"""
Main callback for the firewall commands.
Args:
ctx (typer.Context): The Typer context object.
"""
LOG.info("firewall callback started")
@firewall_app.command(
cls=SafetyCLICommand,
name=UNINSTALL_CMD_NAME,
help=UNINSTALL_HELP,
options_metavar="[OPTIONS]",
context_settings={
"allow_extra_args": True,
"ignore_unknown_options": True,
CONTEXT_COMMAND_TYPE: CommandType.BETA,
CONTEXT_FEATURE_TYPE: FeatureType.FIREWALL,
},
)
@handle_cmd_exception
@notify
def uninstall(ctx: typer.Context):
console.print()
console.print(MSG_UNINSTALL_EXPLANATION)
console.print()
prompt = "Uninstall?"
should_uninstall = (
Prompt.ask(
prompt=prompt,
choices=["y", "n"],
default="y",
show_default=True,
console=console,
).lower()
== "y"
)
if not should_uninstall:
sys.exit(EXIT_CODE_OK)
console.print()
for msg in MSG_UNINSTALL_CONFIG:
console.print(msg)
# TODO: Make it robust. The reset per tool should be included in remove
# interceptors
reset_system()
# TODO: support reset project files
console.print(MSG_UNINSTALL_WRAPPERS)
interceptor = create_interceptor()
interceptor.remove_interceptors()
console.print()
console.print(MSG_UNINSTALL_SUCCESS)
console.print()
console.print(MSG_REQ_FILE_LINE)
console.print()
console.print(MSG_FEEDBACK)
console.print()
prompt = "Feedback (or enter to exit)"
feedback = Prompt.ask(prompt)
feedback = None if len(feedback) <= 0 else feedback
emit_firewall_disabled(event_bus=ctx.obj.event_bus, reason=feedback)
if feedback:
console.print()
console.print("Thank you for your feedback!")
class ToolChoice(str, Enum):
pip = "pip"
poetry = "poetry"
uv = "uv"
npm = "npm"
@firewall_app.command(
cls=SafetyCLICommand,
name=INIT_CMD_NAME,
help=INIT_HELP,
options_metavar="[OPTIONS]",
context_settings={
"allow_extra_args": True,
"ignore_unknown_options": True,
CONTEXT_COMMAND_TYPE: CommandType.BETA,
CONTEXT_FEATURE_TYPE: FeatureType.FIREWALL,
},
)
@handle_cmd_exception
@notify
def init(
ctx: typer.Context,
tool: Optional[List[ToolChoice]] = typer.Option(
None,
"--tool",
help="Specify one or more tools to initialize. If not specified, all tools will be used.",
),
):
console.print()
interceptor = create_interceptor()
# If no tools specified, use all tools
if not tool:
selected_tools = list(interceptor.tools.keys())
console.print("No tools specified. Using all available tools.")
console.line()
else:
selected_tools = [t.value for t in tool]
console.print(
f"Initializing safety firewall for tools: {', '.join(selected_tools)}"
)
interceptor.install_interceptors(tools=selected_tools)
console.print()
console.print(MSG_INIT_SUCCESS.format(", ".join(selected_tools)))
MSG_COMMAND_TO_RUN = "`source ~/.safety/.safety_profile`"
MSG_SETUP_NEXT_STEPS_MANUAL_STEP = (
"(Don't forget to restart the terminal now!)"
if sys.platform == "win32"
else f"(Don't forget to run {MSG_COMMAND_TO_RUN} now!)"
)
console.print()
console.print(MSG_SETUP_NEXT_STEPS_MANUAL_STEP)

View File

@@ -0,0 +1,25 @@
MSG_UNINSTALL_EXPLANATION = "Would you like to uninstall Safety Firewall on this machine? Doing so will mean you are no longer protected from malicious or vulnerable packages."
MSG_UNINSTALL_SUCCESS = "Safety Firewall has been uninstalled from your machine. Note that your individual requirements files may still reference Safety Firewall. You can remove these references by removing the following line from your requirements files:"
MSG_REQ_FILE_LINE = "-i https://pkgs.safetycli.com/repository/public/pypi/simple/"
MSG_FEEDBACK = "We're sorry to see you go. If you have any feedback on how we can do better, we'd love to hear it. Otherwise hit enter to exit."
UNINSTALL_HELP = "Uninstall Safety Firewall from your machine."
FIREWALL_CMD_NAME = "firewall"
UNINSTALL_CMD_NAME = "uninstall"
FIREWALL_HELP = "[BETA] Manage Safety Firewall settings."
MSG_UNINSTALL_CONFIG = (
"Removing global configuration for pip from: ~/.config/pip/pip.conf",
"Removing global configuration for uv from: uv.toml",
)
MSG_UNINSTALL_WRAPPERS = "Removing aliases to safety from config files"
INIT_CMD_NAME = "init"
INIT_HELP = "Initialize Safety Firewall on this machine."
MSG_INIT_SUCCESS = "Safety Firewall has been initialized on your machine. The following tools are now protected: {}"

View File

@@ -0,0 +1,4 @@
from .utils import register_event_handlers
__all__ = ["register_event_handlers"]

View File

@@ -0,0 +1,28 @@
from typing import TYPE_CHECKING
from safety.events.handlers import EventHandler
from safety.events.types import EventBusReadyEvent
from safety.events.utils import emit_firewall_heartbeat
from safety.tool import ToolInspector
if TYPE_CHECKING:
from safety.events.event_bus import EventBus
class HeartbeatInspectionEventHandler(EventHandler[EventBusReadyEvent]):
"""
Inspect the system for installed tools and send an emit
a firewall heartbeat event.
"""
def __init__(self, event_bus: "EventBus") -> None:
super().__init__()
self.event_bus = event_bus
async def handle(self, event: EventBusReadyEvent):
ctx = event.payload.ctx
inspector = ToolInspector(timeout=1.0)
tools = await inspector.inspect_all_tools()
emit_firewall_heartbeat(self.event_bus, ctx, tools=tools)

View File

@@ -0,0 +1,40 @@
from typing import TYPE_CHECKING
from safety_schemas.models.events import EventType
from safety.events.event_bus import EventBus
from safety.events.types import InternalEventType
from .handlers import HeartbeatInspectionEventHandler
if TYPE_CHECKING:
from safety.models import SafetyCLI
def register_event_handlers(event_bus: "EventBus", obj: "SafetyCLI") -> None:
"""
Subscribes to the firewall events that are relevant to the current context.
"""
handle_inspection = HeartbeatInspectionEventHandler(event_bus=event_bus)
event_bus.subscribe([InternalEventType.EVENT_BUS_READY], handle_inspection)
if sec_events_handler := obj.security_events_handler:
event_bus.subscribe(
[
EventType.FIREWALL_CONFIGURED,
EventType.FIREWALL_HEARTBEAT,
EventType.FIREWALL_DISABLED,
EventType.PACKAGE_INSTALLED,
EventType.PACKAGE_UNINSTALLED,
EventType.PACKAGE_UPDATED,
EventType.TOOL_COMMAND_EXECUTED,
EventType.INIT_STARTED,
EventType.FIREWALL_SETUP_RESPONSE_CREATED,
EventType.FIREWALL_SETUP_COMPLETED,
EventType.CODEBASE_DETECTION_STATUS,
EventType.CODEBASE_SETUP_RESPONSE_CREATED,
EventType.CODEBASE_SETUP_COMPLETED,
EventType.INIT_SCAN_COMPLETED,
EventType.INIT_EXITED,
],
sec_events_handler,
)