updates
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
765
Backend/venv/lib/python3.12/site-packages/safety/init/command.py
Normal file
765
Backend/venv/lib/python3.12/site-packages/safety/init/command.py
Normal file
@@ -0,0 +1,765 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Optional,
|
||||
Iterator,
|
||||
)
|
||||
|
||||
from rich.live import Live
|
||||
from rich.padding import Padding
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Prompt
|
||||
from rich.syntax import Syntax
|
||||
from rich.table import Table
|
||||
from rich.text import Text
|
||||
|
||||
from safety.codebase_utils import load_unverified_project_from_config
|
||||
from safety.events.utils.emission import (
|
||||
emit_codebase_detection_status,
|
||||
emit_codebase_setup_completed,
|
||||
emit_init_exited,
|
||||
emit_init_scan_completed,
|
||||
)
|
||||
from safety.init.models import StepTracker
|
||||
from safety_schemas.models.events.types import ToolType
|
||||
|
||||
|
||||
from .render import (
|
||||
ask_codebase_setup,
|
||||
ask_continue,
|
||||
ask_firewall_setup,
|
||||
progressive_print,
|
||||
render_header,
|
||||
typed_print,
|
||||
)
|
||||
from safety.scan.init_scan import start_scan
|
||||
from ..cli_util import (
|
||||
CommandType,
|
||||
FeatureType,
|
||||
SafetyCLICommand,
|
||||
SafetyCLISubGroup,
|
||||
)
|
||||
from safety.error_handlers import handle_cmd_exception
|
||||
import typer
|
||||
|
||||
|
||||
from safety.init.constants import (
|
||||
MSG_ANALYZE_CODEBASE_TITLE,
|
||||
MSG_CODEBASE_FAILED_TO_SCAN,
|
||||
MSG_CODEBASE_NOT_CONFIGURED,
|
||||
MSG_CODEBASE_URL_DESCRIPTION,
|
||||
MSG_COMPLETE_SECURED,
|
||||
MSG_COMPLETE_TOOL_SECURED,
|
||||
MSG_FIREWALL_UNINSTALL,
|
||||
MSG_LAST_MANUAL_STEP,
|
||||
MSG_NO_VULNERABILITIES_FOUND,
|
||||
MSG_NO_VULNS_CODEBASE_URL_DESCRIPTION,
|
||||
MSG_OPEN_DASHBOARD_PROMPT,
|
||||
MSG_SETUP_CODEBASE_NO_PROJECT,
|
||||
MSG_SETUP_COMPLETE_SUBTITLE,
|
||||
MSG_SETUP_COMPLETE_TITLE,
|
||||
MSG_SETUP_INCOMPLETE,
|
||||
MSG_SETUP_NEXT_STEPS,
|
||||
MSG_SETUP_NEXT_STEPS_MANUAL_STEP,
|
||||
MSG_SETUP_NEXT_STEPS_NO_PROJECT,
|
||||
MSG_SETUP_NEXT_STEPS_NO_VULNS,
|
||||
MSG_SETUP_NEXT_STEPS_SUBTITLE,
|
||||
MSG_SETUP_PACKAGE_FIREWALL_DESCRIPTION,
|
||||
MSG_SETUP_PACKAGE_FIREWALL_TITLE,
|
||||
MSG_SETUP_CODEBASE_DESCRIPTION,
|
||||
MSG_SETUP_CODEBASE_TITLE,
|
||||
CODEBASE_INIT_CMD_NAME,
|
||||
CODEBASE_INIT_HELP,
|
||||
CODEBASE_INIT_DIRECTORY_HELP,
|
||||
MSG_TOOLS_NOT_CONFIGURED,
|
||||
MSG_WELCOME_TITLE,
|
||||
MSG_WELCOME_DESCRIPTION,
|
||||
)
|
||||
from safety.init.main import create_project, launch_auth_if_needed, setup_firewall
|
||||
from safety.console import (
|
||||
get_spinner_animation,
|
||||
main_console as console,
|
||||
should_use_ascii,
|
||||
)
|
||||
from ..tool.main import (
|
||||
configure_local_directory,
|
||||
find_local_tool_files,
|
||||
)
|
||||
|
||||
from ..constants import CONTEXT_COMMAND_TYPE, CONTEXT_FEATURE_TYPE
|
||||
|
||||
from safety.decorators import notify
|
||||
from safety.events.utils import emit_firewall_configured, emit_init_started
|
||||
from safety_schemas.models.events.payloads import AliasConfig, IndexConfig, InitExitStep
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import typer
|
||||
from safety.scan.init_scan import ScanResult
|
||||
|
||||
try:
|
||||
from typing import Annotated # type: ignore
|
||||
except ImportError:
|
||||
from typing_extensions import Annotated
|
||||
|
||||
init_app = typer.Typer(rich_markup_mode="rich", cls=SafetyCLISubGroup)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InitScanState:
|
||||
"""
|
||||
Class to track scan state for vulnerability scans
|
||||
|
||||
Attributes:
|
||||
scan_id: ID of the scan
|
||||
dependencies: Number of dependencies found
|
||||
critical: Count of critical vulnerabilities
|
||||
high: Count of high severity vulnerabilities
|
||||
medium: Count of medium severity vulnerabilities
|
||||
low: Count of low severity vulnerabilities
|
||||
fixes: Number of fixes available
|
||||
fixed_vulns: Number of vulnerabilities with fixes
|
||||
url: URL to view the scan results
|
||||
completed: Whether the scan has completed
|
||||
progress: Percentage progress of the scan
|
||||
status_message: Current status message from the scanner
|
||||
status_action: Current action being performed by the scanner
|
||||
current_file: Current file being processed
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.scan_id: Optional[str] = None
|
||||
self.dependencies: int = 0
|
||||
self.critical: int = 0
|
||||
self.high: int = 0
|
||||
self.medium: int = 0
|
||||
self.low: int = 0
|
||||
self.others: int = 0
|
||||
self.vulns_count: int = 0
|
||||
self.fixes: int = 0
|
||||
self.fixed_vulns: int = 0
|
||||
self.codebase_url: Optional[str] = None
|
||||
self.completed: bool = False
|
||||
self.progress: int = 0
|
||||
self.status_message: Optional[str] = None
|
||||
self.status_action: Optional[str] = None
|
||||
self.current_file: Optional[str] = None
|
||||
|
||||
|
||||
def generate_summary(state: InitScanState, spinner_phase=0) -> Text:
|
||||
"""
|
||||
Generate the summary text based on current scan state
|
||||
"""
|
||||
spinner = get_spinner_animation()
|
||||
|
||||
text_markup = f"Tested {state.dependencies} dependenc{'y' if state.dependencies == 1 else 'ies'} for security issues\n"
|
||||
text_markup += "\nFound:\n\n"
|
||||
|
||||
categories = [
|
||||
{
|
||||
"name": "CRITICAL",
|
||||
"icon": ":fire:",
|
||||
"style": "bold red",
|
||||
"dim_style": "dim red",
|
||||
"count_attr": "critical",
|
||||
"spinner_offset": 0,
|
||||
},
|
||||
{
|
||||
"name": "HIGH",
|
||||
"icon": ":yellow_circle:",
|
||||
"style": "bold yellow",
|
||||
"dim_style": "dim yellow",
|
||||
"count_attr": "high",
|
||||
"spinner_offset": 2,
|
||||
},
|
||||
{
|
||||
"name": "MEDIUM",
|
||||
"icon": "!!",
|
||||
"style": "yellow",
|
||||
"dim_style": "dim yellow",
|
||||
"count_attr": "medium",
|
||||
"spinner_offset": 4,
|
||||
},
|
||||
{
|
||||
"name": "LOW",
|
||||
"icon": ":icon_info: ",
|
||||
"style": "bold blue",
|
||||
"dim_style": "dim blue",
|
||||
"count_attr": "low",
|
||||
"spinner_offset": 6,
|
||||
},
|
||||
{
|
||||
"name": "OTHER",
|
||||
"icon": "**",
|
||||
"style": "blue",
|
||||
"dim_style": "dim blue",
|
||||
"count_attr": "others",
|
||||
"spinner_offset": 8,
|
||||
},
|
||||
]
|
||||
|
||||
# No vulnerabilities case
|
||||
prepend_text_codebase_url = MSG_CODEBASE_URL_DESCRIPTION
|
||||
|
||||
if state.completed and state.vulns_count <= 0:
|
||||
text_markup += MSG_NO_VULNERABILITIES_FOUND
|
||||
prepend_text_codebase_url = MSG_NO_VULNS_CODEBASE_URL_DESCRIPTION
|
||||
else:
|
||||
for category in categories:
|
||||
category_count = getattr(state, category["count_attr"])
|
||||
|
||||
if state.completed and category_count == 0:
|
||||
continue
|
||||
|
||||
style = category["style"]
|
||||
text_markup += f"[{style}]{category['icon']} {category['name']}: [/{style}]"
|
||||
|
||||
if category_count > 0:
|
||||
vulns_word = f"vulnerabilit{'y' if category_count == 1 else 'ies'}"
|
||||
text_markup += f"[{style}]{category_count}[/{style}] {vulns_word}\n"
|
||||
else:
|
||||
spinner_status = spinner[
|
||||
(spinner_phase + category["spinner_offset"]) % len(spinner)
|
||||
]
|
||||
style = category["dim_style"]
|
||||
text_markup += f"[{style}]{spinner_status}[/{style}] Scanning\n"
|
||||
|
||||
# Show fixes info if we have vulnerabilities
|
||||
if state.vulns_count > 0 and state.fixes is not None:
|
||||
text_markup += "\n"
|
||||
|
||||
if state.fixes > 0:
|
||||
fix_word = f"fix{'' if state.fixes == 1 else 'es'}"
|
||||
vulns_word = f"vulnerabilit{'y' if state.fixed_vulns == 1 else 'ies'}"
|
||||
text_markup += f":sparkles: [green]{state.fixes}[/green] automatic {fix_word} available, resolving {state.fixed_vulns} {vulns_word}\n"
|
||||
else:
|
||||
text_markup += (
|
||||
" No automatic fixes available for these vulnerabilities\n"
|
||||
)
|
||||
|
||||
# Dashboard link if URL is available
|
||||
if state.codebase_url is not None:
|
||||
text_markup += f"\n{prepend_text_codebase_url}[blue underline]:link: {state.codebase_url}\n[/blue underline]"
|
||||
|
||||
elif state.completed:
|
||||
text_markup += "\n"
|
||||
|
||||
return console.render_str(text_markup)
|
||||
|
||||
|
||||
def generate_status_updates(state: InitScanState, spinner_phase: int = 0) -> Text:
|
||||
"""
|
||||
Generate text displaying current status updates and progress information
|
||||
|
||||
Args:
|
||||
state: The InitScanState object containing status information
|
||||
spinner_phase: Current phase of the spinner animation
|
||||
|
||||
Returns:
|
||||
Rich Text object containing formatted status updates
|
||||
"""
|
||||
spinner = get_spinner_animation()
|
||||
|
||||
markup_text = f"[cyan]{spinner[spinner_phase % len(spinner)]} Scanning...[/cyan]"
|
||||
|
||||
# Display current status message if available
|
||||
if state.status_message:
|
||||
action_symbol = {
|
||||
"init": ":arrows_counterclockwise:",
|
||||
"scanning": ":magnifying_glass_tilted_left:",
|
||||
"uploading": ":cloud: ",
|
||||
"complete": ":white_heavy_check_mark:",
|
||||
"error": ":cross_mark:",
|
||||
}
|
||||
status_key = state.status_action if state.status_action is not None else "init"
|
||||
action_symbol = action_symbol.get(status_key, ":information_source: ")
|
||||
|
||||
markup_text = f"\n[bold cyan]{action_symbol} Status: [/bold cyan][cyan]{state.status_message}[/cyan]\n"
|
||||
|
||||
# If we're processing a file, show that
|
||||
if state.current_file and state.status_action == "scanning":
|
||||
markup_text += f"[bold cyan]:file_folder: Current file: [/bold cyan][dim cyan]{state.current_file}[/dim cyan]\n"
|
||||
|
||||
# Ensure progress is capped at 100%
|
||||
display_progress = min(state.progress, 100)
|
||||
markup_text += f"[cyan]:bar_chart: Progress: {display_progress}%[/cyan]\n"
|
||||
|
||||
return console.render_str(markup_text)
|
||||
|
||||
|
||||
def process_scan_results(
|
||||
scan_results: Iterator["ScanResult"], state: InitScanState
|
||||
) -> None:
|
||||
"""Process the scan iterator and update state from typed result models
|
||||
|
||||
Args:
|
||||
scan_results: Iterator yielding scan results from init_scan
|
||||
state: The InitScanState object to update with scan results
|
||||
"""
|
||||
# Import the scan result types to handle typed results
|
||||
from safety.scan.init_scan import (
|
||||
ScanResultType,
|
||||
)
|
||||
|
||||
try:
|
||||
for result in scan_results:
|
||||
# Now result is a typed model with proper attributes
|
||||
if result.type == ScanResultType.INIT:
|
||||
# Initial state with dependency count
|
||||
state.dependencies = result.dependencies
|
||||
state.status_message = "Initializing scan"
|
||||
state.status_action = "init"
|
||||
|
||||
elif result.type == ScanResultType.PROGRESS:
|
||||
# Update all the state fields from the progress result
|
||||
# Type checker knows result is ProgressScanResult
|
||||
# Ensure progress never exceeds 100%
|
||||
state.progress = min(result.percent, 100)
|
||||
state.dependencies = result.dependencies
|
||||
|
||||
# Track current file being processed
|
||||
state.current_file = result.file
|
||||
state.status_message = f"Processing {result.file}"
|
||||
state.status_action = "scanning"
|
||||
|
||||
# Update severity counts if present
|
||||
if result.critical is not None:
|
||||
state.critical = result.critical
|
||||
if result.high is not None:
|
||||
state.high = result.high
|
||||
if result.medium is not None:
|
||||
state.medium = result.medium
|
||||
if result.low is not None:
|
||||
state.low = result.low
|
||||
if result.others is not None:
|
||||
state.others = result.others
|
||||
|
||||
# Update vulnerability count
|
||||
if result.vulns_count is not None:
|
||||
state.vulns_count = result.vulns_count
|
||||
|
||||
# Update fix information if present
|
||||
if result.fixes is not None:
|
||||
state.fixes = result.fixes
|
||||
if result.fixed_vulns is not None:
|
||||
state.fixed_vulns = result.fixed_vulns
|
||||
|
||||
elif result.type == ScanResultType.STATUS:
|
||||
# Generic status update
|
||||
state.status_message = result.message
|
||||
state.status_action = result.action
|
||||
if result.percent is not None:
|
||||
state.progress = min(result.percent, 100)
|
||||
|
||||
elif result.type == ScanResultType.UPLOADING:
|
||||
# Status update for uploading phase
|
||||
state.status_message = result.message
|
||||
state.status_action = "uploading"
|
||||
if result.percent is not None:
|
||||
state.progress = min(result.percent, 100)
|
||||
|
||||
elif result.type == ScanResultType.COMPLETE:
|
||||
# Final update with complete data
|
||||
# Type checker knows result is CompleteScanResult
|
||||
state.progress = 100
|
||||
state.dependencies = result.dependencies
|
||||
state.critical = result.critical
|
||||
state.high = result.high
|
||||
state.medium = result.medium
|
||||
state.low = result.low
|
||||
state.others = result.others
|
||||
state.fixes = result.fixes
|
||||
state.fixed_vulns = result.fixed_vulns
|
||||
state.status_message = "Scan completed"
|
||||
state.status_action = "complete"
|
||||
state.vulns_count = result.vulns_count
|
||||
|
||||
# Update project URL if available
|
||||
if result.codebase_url:
|
||||
state.codebase_url = result.codebase_url
|
||||
|
||||
if result.scan_id:
|
||||
state.scan_id = result.scan_id
|
||||
|
||||
# We're done processing
|
||||
state.completed = True
|
||||
|
||||
# Add a small delay between updates to allow UI thread to refresh
|
||||
time.sleep(0.05)
|
||||
except Exception as e:
|
||||
console.print(f"Error processing scan results: {e}", style="bold red")
|
||||
state.status_message = f"Error: {str(e)}"
|
||||
state.status_action = "error"
|
||||
finally:
|
||||
# Ensure we mark as completed even if there was an exception
|
||||
state.completed = True
|
||||
|
||||
|
||||
def init_scan_ui(ctx: "typer.Context", prompt_user: bool = False) -> InitScanState:
|
||||
"""
|
||||
Initialize and run a scan for the init command, showing a live UI with scan progress.
|
||||
Uses the start_scan function to get an iterator of scan results and displays UI based on them.
|
||||
|
||||
Args:
|
||||
ctx: The Typer context object containing configuration and project information
|
||||
"""
|
||||
# Initialize state for tracking scan progress
|
||||
state = InitScanState()
|
||||
|
||||
# Set up scan parameters and get the scan iterator
|
||||
target = ctx.obj.project.project_path.parent
|
||||
use_server_matching = False
|
||||
|
||||
# Start the scan using the dedicated function
|
||||
scan_results = start_scan(
|
||||
ctx=ctx,
|
||||
target=target,
|
||||
use_server_matching=use_server_matching,
|
||||
auth_type=ctx.obj.auth.client.get_authentication_type(),
|
||||
is_authenticated=ctx.obj.auth.client.is_using_auth_credentials(),
|
||||
client=ctx.obj.auth.client,
|
||||
project=ctx.obj.project,
|
||||
platform_enabled=ctx.obj.platform_enabled,
|
||||
)
|
||||
|
||||
# Process the scan results in a separate thread
|
||||
scan_thread = threading.Thread(
|
||||
target=process_scan_results, args=(scan_results, state)
|
||||
)
|
||||
scan_thread.daemon = True
|
||||
scan_thread.start()
|
||||
|
||||
# Handle UI updates in the main thread
|
||||
spinner_phase = 0
|
||||
render_header(
|
||||
MSG_ANALYZE_CODEBASE_TITLE.format(project_name=ctx.obj.project.id),
|
||||
emoji=":magnifying_glass_tilted_left:",
|
||||
)
|
||||
time.sleep(0.8)
|
||||
|
||||
# Detect if running on Windows
|
||||
is_windows = sys.platform == "win32" or should_use_ascii()
|
||||
|
||||
# Alternate screen in Windows is buggy, so we disable it
|
||||
live_kwargs = {
|
||||
"refresh_per_second": 10,
|
||||
"screen": False if is_windows else True,
|
||||
"transient": False,
|
||||
}
|
||||
|
||||
refresh_sleep = 0.1
|
||||
|
||||
with Live(**live_kwargs) as live:
|
||||
while not state.completed or scan_thread.is_alive():
|
||||
# Update spinner phase for animation
|
||||
spinner_phase = (spinner_phase + 1) % 10
|
||||
|
||||
# Summary information shown below status updates
|
||||
summary = generate_summary(state, spinner_phase)
|
||||
|
||||
if is_windows:
|
||||
content = summary
|
||||
else:
|
||||
# Create a container for all UI elements
|
||||
container = Table.grid(padding=0, expand=True)
|
||||
container.add_row(None)
|
||||
container.add_row(
|
||||
Panel(
|
||||
generate_status_updates(state, spinner_phase),
|
||||
border_style="cyan",
|
||||
padding=(0, 1),
|
||||
)
|
||||
)
|
||||
container.add_row(None)
|
||||
container.add_row(summary)
|
||||
content = container
|
||||
|
||||
# Display the updated UI
|
||||
live.update(content)
|
||||
|
||||
time.sleep(refresh_sleep)
|
||||
|
||||
# Last sync
|
||||
if state.completed:
|
||||
live.update(generate_summary(state, spinner_phase))
|
||||
time.sleep(2)
|
||||
|
||||
# Windows is not using alternate screen, so summary is already rendered
|
||||
if not is_windows:
|
||||
# Final update to ensure completion state is shown
|
||||
console.print(generate_summary(state))
|
||||
|
||||
if state.codebase_url:
|
||||
typed_print(MSG_OPEN_DASHBOARD_PROMPT, end_line=False)
|
||||
should_open = "y"
|
||||
|
||||
if prompt_user:
|
||||
should_open = Prompt.ask(
|
||||
"",
|
||||
choices=["y", "n", "Y", "N"],
|
||||
default="y",
|
||||
show_default=False,
|
||||
show_choices=False,
|
||||
console=console,
|
||||
).lower()
|
||||
|
||||
if should_open == "y":
|
||||
typer.launch(state.codebase_url)
|
||||
|
||||
return state
|
||||
|
||||
|
||||
@init_app.command(
|
||||
cls=SafetyCLICommand,
|
||||
help=CODEBASE_INIT_HELP,
|
||||
name=CODEBASE_INIT_CMD_NAME,
|
||||
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,
|
||||
directory: Annotated[
|
||||
Path,
|
||||
typer.Argument( # type: ignore
|
||||
exists=True,
|
||||
file_okay=False,
|
||||
dir_okay=True,
|
||||
writable=False,
|
||||
readable=True,
|
||||
resolve_path=True,
|
||||
show_default=False,
|
||||
help=CODEBASE_INIT_DIRECTORY_HELP,
|
||||
),
|
||||
] = Path("."),
|
||||
):
|
||||
emit_init_started(ctx.obj.event_bus, ctx)
|
||||
# TODO: check if tty is available
|
||||
tracker = StepTracker()
|
||||
try:
|
||||
do_init(ctx, directory, tracker, prompt_user=console.is_interactive)
|
||||
except KeyboardInterrupt as e:
|
||||
emit_init_exited(ctx.obj.event_bus, ctx, exit_step=tracker.current_step)
|
||||
raise e
|
||||
|
||||
|
||||
def do_init(
|
||||
ctx: typer.Context, directory: Path, tracker: StepTracker, prompt_user: bool = True
|
||||
):
|
||||
"""
|
||||
Initialize Safety CLI with the new onboarding flow.
|
||||
|
||||
Args:
|
||||
ctx: The CLI context
|
||||
directory: The target directory to initialize
|
||||
prompt_user: Whether to prompt the user for input or use defaults
|
||||
"""
|
||||
project_dir = directory.resolve()
|
||||
|
||||
typed_print(MSG_WELCOME_TITLE)
|
||||
progressive_print(MSG_WELCOME_DESCRIPTION)
|
||||
|
||||
tracker.current_step = InitExitStep.PRE_AUTH
|
||||
org_slug = launch_auth_if_needed(ctx, console)
|
||||
tracker.current_step = InitExitStep.POST_AUTH
|
||||
|
||||
render_header(MSG_SETUP_PACKAGE_FIREWALL_TITLE, margin_right=1)
|
||||
console.print(MSG_SETUP_PACKAGE_FIREWALL_DESCRIPTION)
|
||||
|
||||
console.print(
|
||||
Syntax(
|
||||
MSG_FIREWALL_UNINSTALL, "bash", theme="monokai", background_color="default"
|
||||
)
|
||||
)
|
||||
|
||||
completed_tools = ""
|
||||
all_completed = False
|
||||
all_missing = True
|
||||
|
||||
status = {
|
||||
ToolType.PIP: {
|
||||
"alias": AliasConfig(is_configured=False),
|
||||
"index": IndexConfig(is_configured=False),
|
||||
},
|
||||
ToolType.POETRY: {
|
||||
"alias": AliasConfig(is_configured=False),
|
||||
"index": IndexConfig(is_configured=False),
|
||||
},
|
||||
ToolType.UV: {
|
||||
"alias": AliasConfig(is_configured=False),
|
||||
"index": IndexConfig(is_configured=False),
|
||||
},
|
||||
ToolType.NPM: {
|
||||
"alias": AliasConfig(is_configured=False),
|
||||
"index": IndexConfig(is_configured=False),
|
||||
},
|
||||
}
|
||||
|
||||
tracker.current_step = InitExitStep.PRE_FIREWALL_SETUP
|
||||
if ask_firewall_setup(ctx, prompt_user):
|
||||
completed_tools, all_completed, all_missing, status = setup_firewall(
|
||||
ctx, status, org_slug, console
|
||||
)
|
||||
console.line()
|
||||
ask_continue(ctx, prompt_user)
|
||||
console.line()
|
||||
|
||||
tracker.current_step = InitExitStep.POST_FIREWALL_SETUP
|
||||
|
||||
render_header(MSG_SETUP_CODEBASE_TITLE, emoji=":locked:")
|
||||
console.print(MSG_SETUP_CODEBASE_DESCRIPTION)
|
||||
|
||||
project_scan_state = None
|
||||
|
||||
tracker.current_step = InitExitStep.PRE_CODEBASE_SETUP
|
||||
local_files = find_local_tool_files(project_dir)
|
||||
|
||||
emit_codebase_detection_status(
|
||||
event_bus=ctx.obj.event_bus,
|
||||
ctx=ctx,
|
||||
detected=any(local_files),
|
||||
detected_files=local_files if local_files else None,
|
||||
)
|
||||
|
||||
if local_files:
|
||||
progressive_print(
|
||||
[
|
||||
f":pushpin: We found a `{file.name}` file in this directory."
|
||||
for file in local_files
|
||||
]
|
||||
)
|
||||
|
||||
console.line()
|
||||
|
||||
if ask_codebase_setup(ctx, prompt_user):
|
||||
unverified_project = load_unverified_project_from_config(
|
||||
project_root=project_dir
|
||||
)
|
||||
link_behavior = "prompt"
|
||||
|
||||
if unverified_project.created:
|
||||
link_behavior = "always"
|
||||
|
||||
project_created, project_status = create_project(
|
||||
ctx,
|
||||
console,
|
||||
project_dir,
|
||||
unverified_project=unverified_project,
|
||||
link_behavior=link_behavior,
|
||||
)
|
||||
|
||||
configure_local_directory(project_dir, org_slug, ctx.obj.project.id)
|
||||
|
||||
emit_codebase_setup_completed(
|
||||
event_bus=ctx.obj.event_bus,
|
||||
ctx=ctx,
|
||||
is_created=project_created,
|
||||
codebase_id=ctx.obj.project.id if project_created else None,
|
||||
)
|
||||
|
||||
if project_created:
|
||||
console.print(
|
||||
"\n"
|
||||
+ f"{ctx.obj.project.id} codebase {project_status} :white_heavy_check_mark:"
|
||||
)
|
||||
else:
|
||||
progressive_print([":x: Failed to create codebase"])
|
||||
|
||||
console.line()
|
||||
|
||||
tracker.current_step = InitExitStep.PRE_SCAN
|
||||
project_scan_state = init_scan_ui(ctx, prompt_user)
|
||||
tracker.current_step = InitExitStep.POST_SCAN
|
||||
emit_init_scan_completed(
|
||||
event_bus=ctx.obj.event_bus,
|
||||
ctx=ctx,
|
||||
scan_id=project_scan_state.scan_id,
|
||||
)
|
||||
else:
|
||||
console.print(MSG_SETUP_CODEBASE_NO_PROJECT)
|
||||
|
||||
tracker.current_step = InitExitStep.POST_CODEBASE_SETUP
|
||||
|
||||
console.line()
|
||||
render_header(MSG_SETUP_COMPLETE_TITLE, emoji=":trophy:")
|
||||
|
||||
is_setup_complete = all_completed and project_scan_state
|
||||
|
||||
wrap_up_msg = []
|
||||
|
||||
if all_completed:
|
||||
wrap_up_msg.append(
|
||||
MSG_COMPLETE_TOOL_SECURED.format(
|
||||
tools=completed_tools,
|
||||
firewall_url="https://platform.safetycli.com/firewall/",
|
||||
)
|
||||
)
|
||||
elif all_missing:
|
||||
wrap_up_msg.append(MSG_TOOLS_NOT_CONFIGURED)
|
||||
else:
|
||||
wrap_up_msg.append(MSG_SETUP_INCOMPLETE)
|
||||
|
||||
wrap_up_msg.append("")
|
||||
|
||||
if project_scan_state:
|
||||
if project_scan_state.scan_id:
|
||||
wrap_up_msg.append(
|
||||
MSG_COMPLETE_SECURED.format(
|
||||
codebase_url=project_scan_state.codebase_url
|
||||
)
|
||||
)
|
||||
else:
|
||||
msg = (
|
||||
project_scan_state.status_message
|
||||
if project_scan_state.status_message
|
||||
else "Unknown"
|
||||
)
|
||||
wrap_up_msg.append(MSG_CODEBASE_FAILED_TO_SCAN.format(reason=msg))
|
||||
else:
|
||||
wrap_up_msg.append(MSG_CODEBASE_NOT_CONFIGURED)
|
||||
|
||||
if wrap_up_msg:
|
||||
progressive_print(wrap_up_msg)
|
||||
console.line()
|
||||
|
||||
if is_setup_complete:
|
||||
typed_print(MSG_SETUP_COMPLETE_SUBTITLE)
|
||||
console.line()
|
||||
typed_print(MSG_LAST_MANUAL_STEP)
|
||||
console.line()
|
||||
|
||||
render_header(title=MSG_SETUP_NEXT_STEPS_SUBTITLE, emoji=":rocket:")
|
||||
console.line()
|
||||
|
||||
next_steps_msg = MSG_SETUP_NEXT_STEPS
|
||||
|
||||
if not project_scan_state:
|
||||
next_steps_msg = MSG_SETUP_NEXT_STEPS_NO_PROJECT
|
||||
elif project_scan_state.vulns_count <= 0:
|
||||
next_steps_msg = MSG_SETUP_NEXT_STEPS_NO_VULNS
|
||||
|
||||
progressive_print(
|
||||
[Padding(console.render_str(line), (0, 0, 1, 0)) for line in next_steps_msg]
|
||||
)
|
||||
|
||||
console.line()
|
||||
typed_print(MSG_SETUP_NEXT_STEPS_MANUAL_STEP, delay=0.04)
|
||||
console.line()
|
||||
|
||||
# Emit event for firewall configuration
|
||||
emit_firewall_configured(
|
||||
event_bus=ctx.obj.event_bus,
|
||||
status=status,
|
||||
)
|
||||
tracker.current_step = InitExitStep.COMPLETED
|
||||
@@ -0,0 +1,127 @@
|
||||
# Codebase options
|
||||
import sys
|
||||
|
||||
|
||||
CODEBASE_INIT_CMD_NAME = "init"
|
||||
CODEBASE_INIT_HELP = (
|
||||
"[BETA] Used to install Safety Firewall globally, or to initialize a codebase in the current directory."
|
||||
"\nExample: safety init"
|
||||
)
|
||||
CODEBASE_INIT_DIRECTORY_HELP = (
|
||||
"[BETA] Defines a directory for creating a codebase. (default: current directory)\n\n"
|
||||
"[bold]Example: safety init /path/to/codebase[/bold]"
|
||||
)
|
||||
|
||||
# Welcome Section
|
||||
MSG_WELCOME_TITLE = (
|
||||
"\n\nWelcome to Safety, the AI-powered Software Supply Chain Firewall.\n\n"
|
||||
)
|
||||
MSG_WELCOME_DESCRIPTION = [
|
||||
"[bold]Safety is designed to:[/bold]",
|
||||
"1. Work with your existing package manager to block malicious or high-risk packages before they're installed.",
|
||||
"2. Keep track of the dependencies in your codebase, and help you to quickly fix any vulnerabilities in them.",
|
||||
"3. Integrate with your AI assistants to ensure they use secure packages.\n",
|
||||
]
|
||||
|
||||
MSG_NEED_AUTHENTICATION = "To configure firewall and your codebase security settings, you'll need an account.\n"
|
||||
MSG_AUTH_PROMPT = (
|
||||
"Press [bold]R[/bold] to register (it's free & quick), or [bold]L[/bold] to log in"
|
||||
)
|
||||
|
||||
MSG_SETUP_PACKAGE_FIREWALL_TITLE = " Set Up Package Firewall"
|
||||
|
||||
MSG_SETUP_PACKAGE_FIREWALL_DESCRIPTION = "Let's configure Safety Firewall to protect your package installations. This won't change the way you use pip and you'll only notice it when it blocks a malicious or vulnerable package. You can uninstall Firewall at any time with:\n"
|
||||
MSG_FIREWALL_UNINSTALL = "`safety firewall uninstall`\n"
|
||||
|
||||
ASK_HINT = "[Press Enter to continue, n to cancel]"
|
||||
|
||||
MSG_SETUP_PACKAGE_FIREWALL_PROMPT = (
|
||||
f"[bold]Do you want to continue with Firewall installation? {ASK_HINT}[/bold]"
|
||||
)
|
||||
|
||||
SUPPORT_DETAILS = (
|
||||
"[link]support@safetycli.com[/link] (we normally respond within 4 hours)"
|
||||
)
|
||||
|
||||
MSG_SETUP_INCOMPLETE = f"[red bold]x[/red bold] The setup was not completed successfully, reach out to {SUPPORT_DETAILS}"
|
||||
|
||||
MSG_SETUP_PACKAGE_FIREWALL_RESULT = "configured and secured. Safety will analyze package installations for security risks before installation, and warn you if you install vulnerable packages.\n"
|
||||
MSG_SETUP_PACKAGE_FIREWALL_NOTE_STATUS = "To see your firewall status, usage and to configure your firewall security settings visit [link]https://platform.safetycli.com/firewall/[/link]"
|
||||
|
||||
MSG_SETUP_CONTINUE_PROMPT = "[bold][Press Enter to continue][/bold]"
|
||||
|
||||
MSG_SETUP_CODEBASE_TITLE = " Secure Your First Codebase"
|
||||
|
||||
MSG_SETUP_CODEBASE_DESCRIPTION = "Safety monitors your codebase for open source dependency vulnerabilities and risk, surfacing reachable vulnerabilities that pose actual risk, and gives you advice on what to fix and how.\n"
|
||||
|
||||
MSG_SETUP_CODEBASE_PROMPT = (
|
||||
f"[bold]Would you like to secure this codebase with Safety? {ASK_HINT}[/bold]"
|
||||
)
|
||||
|
||||
MSG_SETUP_CODEBASE_NO_PROJECT = "We didn't find any dependency specification files in the current directory. Please navigate to a Python codebase directory and run:\n\n`safety init`"
|
||||
|
||||
MSG_ANALYZE_CODEBASE_TITLE = " Analyze {project_name} for Python Vulnerabilities"
|
||||
|
||||
MSG_NO_VULNERABILITIES_FOUND = " No vulnerabilities found :party_popper:\n\n"
|
||||
MSG_CODEBASE_URL_DESCRIPTION = (
|
||||
":mag_right: View detailed results in your Safety dashboard:\n"
|
||||
)
|
||||
MSG_NO_VULNS_CODEBASE_URL_DESCRIPTION = (
|
||||
":mag_right: Any future vulnerabilities will appear here:\n"
|
||||
)
|
||||
|
||||
MSG_OPEN_DASHBOARD_PROMPT = (
|
||||
f":light_bulb: Open this in a new browser window now? {ASK_HINT}"
|
||||
)
|
||||
|
||||
MSG_COMMAND_TO_RUN = "`source ~/.safety/.safety_profile`"
|
||||
MSG_LAST_MANUAL_STEP = (
|
||||
":yellow_circle: IMPORTANT: At the end, restart the terminal to activate your Safety configuration."
|
||||
if sys.platform == "win32"
|
||||
else f":yellow_circle: IMPORTANT: Run {MSG_COMMAND_TO_RUN} to activate your Safety configuration."
|
||||
)
|
||||
|
||||
MSG_SETUP_COMPLETE_TITLE = " Wrap Up"
|
||||
|
||||
MSG_SETUP_COMPLETE_SUBTITLE = "Almost done! Final step:"
|
||||
|
||||
MSG_TOOLS_NOT_CONFIGURED = "[bold red]x[/bold red] No package managers configured"
|
||||
MSG_CODEBASE_NOT_CONFIGURED = "[bold red]x[/bold red] No codebase configured"
|
||||
MSG_CODEBASE_FAILED_TO_SCAN = (
|
||||
"[bold red]x[/bold red] Failed to complete the codebase scan, reason: {reason}. Reach out to "
|
||||
+ SUPPORT_DETAILS
|
||||
)
|
||||
MSG_COMPLETE_TOOL_SECURED = ":white_heavy_check_mark: {tools} secured - Safety is automatically analyzing all package installations for risk. To configure or audit your installations visit [link]{firewall_url}[/link]"
|
||||
MSG_COMPLETE_SECURED = ":white_heavy_check_mark: Codebase secured - to see your vulnerable packages, visit [link]{codebase_url}[/link]"
|
||||
|
||||
MSG_SETUP_NEXT_STEPS_SUBTITLE = " Next steps:"
|
||||
|
||||
MSG_HELP = f":speech_balloon: Need help or want to give feedback? {SUPPORT_DETAILS}"
|
||||
MSG_DOCS = ":books: Read the docs: [link]https://docs.safetycli.com[/link]"
|
||||
MSG_TEAM = ":busts_in_silhouette: Invite your team: [link]https://platform.safetycli.com/organization/team[/link]"
|
||||
|
||||
|
||||
MSG_SETUP_NEXT_STEPS = (
|
||||
MSG_TEAM,
|
||||
":floppy_disk: Commit `.safety-project.ini` to your Github repository so that your team-members use the same codebase.",
|
||||
":heavy_plus_sign: Add another codebase: `safety init` (run this in the codebase directory)",
|
||||
MSG_DOCS,
|
||||
MSG_HELP,
|
||||
)
|
||||
|
||||
MSG_SETUP_NEXT_STEPS_ERROR = (MSG_HELP, MSG_DOCS)
|
||||
|
||||
MSG_SETUP_NEXT_STEPS_NO_PROJECT = (
|
||||
":heavy_plus_sign: Add a codebase with `safety init` (run this in the codebase directory)",
|
||||
MSG_TEAM,
|
||||
MSG_DOCS,
|
||||
MSG_HELP,
|
||||
)
|
||||
|
||||
MSG_SETUP_NEXT_STEPS_NO_VULNS = (MSG_TEAM, MSG_DOCS, MSG_HELP)
|
||||
|
||||
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!)"
|
||||
)
|
||||
490
Backend/venv/lib/python3.12/site-packages/safety/init/main.py
Normal file
490
Backend/venv/lib/python3.12/site-packages/safety/init/main.py
Normal file
@@ -0,0 +1,490 @@
|
||||
import logging
|
||||
import sys
|
||||
import uuid
|
||||
from rich.prompt import Prompt
|
||||
import typer
|
||||
from rich.console import Console
|
||||
|
||||
from safety.events.utils.emission import emit_firewall_setup_completed
|
||||
from safety.init.render import progressive_print
|
||||
from safety.util import clean_project_id
|
||||
|
||||
from ..tool import configure_system, configure_alias
|
||||
from ..codebase_utils import load_unverified_project_from_config, save_project_info
|
||||
|
||||
from .constants import (
|
||||
MSG_AUTH_PROMPT,
|
||||
MSG_NEED_AUTHENTICATION,
|
||||
MSG_SETUP_INCOMPLETE,
|
||||
MSG_SETUP_PACKAGE_FIREWALL_NOTE_STATUS,
|
||||
MSG_SETUP_PACKAGE_FIREWALL_RESULT,
|
||||
)
|
||||
|
||||
from pathlib import Path
|
||||
from safety_schemas.models import ProjectModel
|
||||
from safety_schemas.models.events.types import ToolType
|
||||
from safety.scan.util import GIT
|
||||
from ..auth.utils import SafetyAuthSession
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, Tuple
|
||||
from safety.scan.render import (
|
||||
print_wait_project_verification,
|
||||
prompt_project_id,
|
||||
prompt_link_project,
|
||||
)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..codebase_utils import UnverifiedProjectModel
|
||||
from ..models import SafetyCLI
|
||||
from .types import FirewallConfigStatus
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def check_project(
|
||||
ctx: typer.Context,
|
||||
session: SafetyAuthSession,
|
||||
console: Console,
|
||||
unverified_project: "UnverifiedProjectModel",
|
||||
git_origin: Optional[str],
|
||||
ask_project_id: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
Check the project against the session and stage, verifying the project if necessary.
|
||||
|
||||
Args:
|
||||
console: The console for output.
|
||||
ctx (typer.Context): The context of the Typer command.
|
||||
session (SafetyAuthSession): The authentication session.
|
||||
unverified_project (UnverifiedProjectModel): The unverified project model.
|
||||
stage (Stage): The current stage.
|
||||
git_origin (Optional[str]): The Git origin URL.
|
||||
ask_project_id (bool): Whether to prompt for the project ID.
|
||||
|
||||
Returns:
|
||||
dict: The result of the project check.
|
||||
"""
|
||||
stage = ctx.obj.auth.stage
|
||||
source = ctx.obj.telemetry.safety_source if ctx.obj.telemetry else None
|
||||
data = {"scan_stage": stage, "safety_source": source}
|
||||
|
||||
PRJ_SLUG_KEY = "project_slug"
|
||||
PRJ_SLUG_SOURCE_KEY = "project_slug_source"
|
||||
PRJ_GIT_ORIGIN_KEY = "git_origin"
|
||||
|
||||
if git_origin:
|
||||
data[PRJ_GIT_ORIGIN_KEY] = git_origin
|
||||
|
||||
if unverified_project.id:
|
||||
data[PRJ_SLUG_KEY] = unverified_project.id
|
||||
|
||||
if unverified_project.created:
|
||||
data[PRJ_SLUG_SOURCE_KEY] = ".safety-project.ini"
|
||||
else:
|
||||
data[PRJ_SLUG_SOURCE_KEY] = "user"
|
||||
elif not git_origin or ask_project_id:
|
||||
fallback_id = unverified_project.project_path.parent.name
|
||||
|
||||
if not fallback_id:
|
||||
# Sometimes the parent directory is empty, so we generate
|
||||
# a random ID
|
||||
fallback_id = str(uuid.uuid4())[:10]
|
||||
|
||||
fallback_id = clean_project_id(fallback_id)
|
||||
|
||||
if ask_project_id:
|
||||
id = prompt_project_id(console, fallback_id)
|
||||
else:
|
||||
id = fallback_id
|
||||
|
||||
unverified_project.id = id
|
||||
|
||||
data[PRJ_SLUG_KEY] = unverified_project.id
|
||||
data[PRJ_SLUG_SOURCE_KEY] = "user"
|
||||
|
||||
status = print_wait_project_verification(
|
||||
console,
|
||||
data[PRJ_SLUG_KEY] if data.get(PRJ_SLUG_KEY, None) else "-",
|
||||
(session.check_project, data),
|
||||
on_error_delay=1,
|
||||
)
|
||||
|
||||
return status
|
||||
|
||||
|
||||
def verify_project(
|
||||
console: Console,
|
||||
ctx: typer.Context,
|
||||
session: SafetyAuthSession,
|
||||
unverified_project: "UnverifiedProjectModel",
|
||||
git_origin: Optional[str],
|
||||
create_if_missing: bool = True,
|
||||
link_behavior: Literal["always", "prompt", "never"] = "prompt",
|
||||
prompt_for_name: bool = False,
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Verify the project, linking it if necessary and saving the verified project information.
|
||||
|
||||
Args:
|
||||
console: The console for output.
|
||||
ctx (typer.Context): The context of the Typer command.
|
||||
session (SafetyAuthSession): The authentication session.
|
||||
unverified_project (UnverifiedProjectModel): The unverified project model.
|
||||
git_origin (Optional[str]): The Git origin URL.
|
||||
create_if_missing (bool): Whether to create codebase if it doesn't exist. Defaults to True.
|
||||
link_behavior (Literal["always", "prompt", "never"]): How to handle codebase linking.
|
||||
- "always": Link without prompting
|
||||
- "prompt": Ask user before linking (default)
|
||||
- "never": Don't link, return early if codebase exists
|
||||
|
||||
Returns:
|
||||
Tuple[bool, Optional[str]]: (success, status_message)
|
||||
- (True, "created"): New codebase was created and verified
|
||||
- (True, "linked"): Existing codebase was linked
|
||||
- (True, "found"): Codebase found but not linked (link_behavior="never")
|
||||
- (False, "not_found"): Codebase not found and create_if_missing=False
|
||||
- (False, None): Verification failed
|
||||
"""
|
||||
|
||||
# Track if we need to ask for project ID (when user declines linking)
|
||||
ask_for_project_id = False
|
||||
asked_codebase_asked_before = False
|
||||
|
||||
while True:
|
||||
result = check_project(
|
||||
ctx,
|
||||
session,
|
||||
console,
|
||||
unverified_project,
|
||||
git_origin,
|
||||
# Ask for project ID when:
|
||||
# 1. User previously declined linking and we need a new ID
|
||||
# 2. We're in prompt mode and don't have a project ID yet
|
||||
ask_project_id=ask_for_project_id,
|
||||
)
|
||||
|
||||
unverified_slug = result.get("slug")
|
||||
project = result.get("project", None)
|
||||
|
||||
# Handle case where project doesn't exist
|
||||
if not project:
|
||||
if not create_if_missing:
|
||||
return (False, "not_found")
|
||||
# Project will be created - continue to verification
|
||||
project_status = (True, "created")
|
||||
|
||||
if prompt_for_name and not asked_codebase_asked_before:
|
||||
asked_codebase_asked_before = True
|
||||
ask_for_project_id = True
|
||||
continue
|
||||
else:
|
||||
# Project exists - handle based on link_behavior
|
||||
if link_behavior == "never":
|
||||
unverified_project.id = project.get("slug")
|
||||
return (True, "found")
|
||||
elif link_behavior == "always":
|
||||
project_status = (True, "linked")
|
||||
elif link_behavior == "prompt":
|
||||
# Prompt user for confirmation
|
||||
prj_name = project.get("name", None)
|
||||
prj_admin_email = project.get("admin", None)
|
||||
|
||||
should_link = prompt_link_project(
|
||||
prj_name=prj_name, prj_admin_email=prj_admin_email, console=console
|
||||
)
|
||||
|
||||
if should_link:
|
||||
project_status = (True, "linked")
|
||||
else:
|
||||
# User declined linking, ask for new project ID and retry
|
||||
unverified_project.id = None
|
||||
ask_for_project_id = True
|
||||
continue
|
||||
|
||||
# Proceed with project verification
|
||||
verified_prj = print_wait_project_verification(
|
||||
console,
|
||||
unverified_slug, # type: ignore
|
||||
(session.project, {"project_id": unverified_slug}),
|
||||
on_error_delay=1,
|
||||
)
|
||||
|
||||
if (
|
||||
verified_prj
|
||||
and isinstance(verified_prj, dict)
|
||||
and verified_prj.get("slug", None)
|
||||
):
|
||||
save_verified_project(
|
||||
ctx,
|
||||
verified_prj["slug"],
|
||||
verified_prj.get("name", None),
|
||||
unverified_project.project_path,
|
||||
verified_prj.get("url", None),
|
||||
verified_prj.get("organization", None),
|
||||
)
|
||||
return project_status
|
||||
else:
|
||||
# Verification failed
|
||||
return (False, None)
|
||||
|
||||
|
||||
def save_verified_project(
|
||||
ctx: typer.Context,
|
||||
slug: str,
|
||||
name: Optional[str],
|
||||
project_path: Path,
|
||||
url_path: Optional[str],
|
||||
organization: Optional[dict],
|
||||
):
|
||||
"""
|
||||
Save the verified project information to the context and project info file.
|
||||
|
||||
Args:
|
||||
ctx (typer.Context): The context of the Typer command.
|
||||
slug (str): The project slug.
|
||||
name (Optional[str]): The project name.
|
||||
project_path (Path): The project path.
|
||||
url_path (Optional[str]): The project URL path.
|
||||
organization (Optional[str]): The project organization.
|
||||
"""
|
||||
ctx.obj.project = ProjectModel(
|
||||
id=slug, name=name, project_path=project_path, url_path=url_path
|
||||
)
|
||||
|
||||
save_project_info(project=ctx.obj.project, project_path=project_path)
|
||||
|
||||
ctx.obj.org = {}
|
||||
if organization:
|
||||
ctx.obj.org = {
|
||||
"name": organization.get("name"),
|
||||
"slug": organization.get("slug"),
|
||||
}
|
||||
|
||||
|
||||
def create_project(
|
||||
ctx: typer.Context,
|
||||
console: Console,
|
||||
target: Path,
|
||||
unverified_project: Optional["UnverifiedProjectModel"] = None,
|
||||
create_if_missing: bool = True,
|
||||
link_behavior: Literal["always", "prompt", "never"] = "prompt",
|
||||
) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Loads existing project from the specified target locations or creates a new project.
|
||||
|
||||
Args:
|
||||
ctx: The CLI context
|
||||
console: The console object
|
||||
target (Path): The target location
|
||||
unverified_project (UnverifiedProjectModel): The unverified project model
|
||||
create_if_missing (bool): Whether to create codebase if it doesn't exist
|
||||
link_behavior (Literal["always", "prompt", "never"]): How to handle codebase linking
|
||||
"""
|
||||
# Load .safety-project.ini
|
||||
if not unverified_project:
|
||||
unverified_project = load_unverified_project_from_config(project_root=target)
|
||||
|
||||
session = ctx.obj.auth.client
|
||||
git_data = GIT(root=target).build_git_data()
|
||||
origin = None
|
||||
|
||||
if git_data:
|
||||
origin = git_data.origin
|
||||
|
||||
if ctx.obj.platform_enabled:
|
||||
result = verify_project(
|
||||
console,
|
||||
ctx,
|
||||
session,
|
||||
unverified_project,
|
||||
origin,
|
||||
create_if_missing,
|
||||
link_behavior,
|
||||
)
|
||||
if ctx.obj.project:
|
||||
ctx.obj.project.git = git_data
|
||||
return result
|
||||
else:
|
||||
console.print("Project creation is not supported for your account.")
|
||||
return (False, None)
|
||||
|
||||
|
||||
def launch_auth_if_needed(ctx: typer.Context, console: Console) -> Optional[str]:
|
||||
"""
|
||||
Launch the authentication flow if needed.
|
||||
|
||||
Args:
|
||||
ctx: The CLI context
|
||||
|
||||
Returns:
|
||||
Optional[str]: The organization slug if authentication is successful
|
||||
"""
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
org_slug = None
|
||||
|
||||
if (
|
||||
not obj.auth
|
||||
or not obj.auth.client
|
||||
or not obj.auth.client.is_using_auth_credentials()
|
||||
):
|
||||
console.print(MSG_NEED_AUTHENTICATION)
|
||||
|
||||
if not console.is_interactive:
|
||||
sys.exit(0)
|
||||
|
||||
auth_choice = Prompt.ask(
|
||||
MSG_AUTH_PROMPT,
|
||||
choices=["r", "l", "R", "L"],
|
||||
default="L",
|
||||
show_choices=False,
|
||||
show_default=True,
|
||||
console=console,
|
||||
).lower()
|
||||
|
||||
from safety.auth.cli import auth_app
|
||||
from safety.cli_util import get_command_for
|
||||
|
||||
login_command = get_command_for(name="login", typer_instance=auth_app)
|
||||
register_command = get_command_for(name="register", typer_instance=auth_app)
|
||||
|
||||
ctx.obj.only_auth_msg = True
|
||||
|
||||
if auth_choice == "r":
|
||||
ctx.invoke(register_command)
|
||||
else:
|
||||
ctx.invoke(login_command)
|
||||
|
||||
try:
|
||||
data = ctx.obj.auth.client.initialize()
|
||||
org_slug = data.get("organization-data", {}).get("slug")
|
||||
except Exception:
|
||||
logger.exception("Unable to load data on the init command")
|
||||
|
||||
return org_slug
|
||||
|
||||
|
||||
def setup_firewall(
|
||||
ctx: Any, status: "FirewallConfigStatus", org_slug: Optional[str], console: Console
|
||||
) -> Tuple[str, bool, bool, "FirewallConfigStatus"]:
|
||||
"""
|
||||
Setup the firewall, this function also handles the output.
|
||||
|
||||
Args:
|
||||
ctx: The CLI context
|
||||
status: The current status of the firewall
|
||||
org_slug: The organization slug
|
||||
console: The console object
|
||||
|
||||
Returns:
|
||||
Tuple[bool, bool, FirewallConfigStatus]: A tuple containing the following:
|
||||
- bool: True if all tools are configured, False otherwise
|
||||
- bool: True if all tools are missing, False otherwise
|
||||
- FirewallConfigStatus: The current status of the firewall
|
||||
"""
|
||||
emoji_check = "[green]:icon_check:[/green]"
|
||||
|
||||
configured_index = configure_system(org_slug)
|
||||
configured_alias = configure_alias()
|
||||
if configured_alias is None:
|
||||
configured_alias = []
|
||||
|
||||
console.line()
|
||||
|
||||
configured = {}
|
||||
if configured_index:
|
||||
configured["index"] = configured_index
|
||||
|
||||
if configured_alias:
|
||||
configured["alias"] = configured_alias
|
||||
|
||||
if any([item[1] for item in configured_index]) or any(
|
||||
[item[1] for item in configured_alias]
|
||||
):
|
||||
for config_type, results in configured.items():
|
||||
for tool_type, path in results:
|
||||
tool_name = tool_type.value
|
||||
index_type = "global"
|
||||
|
||||
tool_config = status[tool_type]
|
||||
is_configured = False
|
||||
|
||||
if path:
|
||||
if config_type == "index":
|
||||
msg = f"Configured {tool_name}’s {index_type} index"
|
||||
else:
|
||||
msg = f"Aliased {tool_name} to safety"
|
||||
|
||||
is_configured = True
|
||||
configured_msg = f"{emoji_check} {msg}"
|
||||
|
||||
path = path.resolve()
|
||||
|
||||
if len(path.parts) > 1:
|
||||
progressive_print([f"{configured_msg} (`{path}`)"])
|
||||
else:
|
||||
progressive_print([configured_msg])
|
||||
else:
|
||||
if config_type == "index":
|
||||
msg = f"{tool_name}’s {index_type} index"
|
||||
else:
|
||||
msg = f"{tool_name} alias"
|
||||
|
||||
prefix_msg = "Failed to configure"
|
||||
emoji = "[red bold]x[/red bold]"
|
||||
|
||||
# If there is a non-compatible global index
|
||||
if tool_type in [ToolType.POETRY]:
|
||||
prefix_msg = "Skipped"
|
||||
msg += " - not supported by poetry"
|
||||
emoji = "[gray bold]-[/gray bold]"
|
||||
# TODO: Set None for now, to avoid mixing
|
||||
# no configured error with skipped.
|
||||
tool_config[config_type] = None
|
||||
else:
|
||||
is_configured = False
|
||||
|
||||
progressive_print([f"{emoji} {prefix_msg} {msg}"])
|
||||
|
||||
if config_obj := tool_config[config_type]:
|
||||
config_obj.is_configured = is_configured
|
||||
|
||||
console.line()
|
||||
else:
|
||||
progressive_print(["[red bold]x[/red bold] Failed to configure system"])
|
||||
|
||||
completed = []
|
||||
missing = []
|
||||
for tool_type, tool_status in status.items():
|
||||
for config_type, config_obj in tool_status.items():
|
||||
if config_obj:
|
||||
if config_obj.is_configured:
|
||||
completed.append(config_obj)
|
||||
else:
|
||||
missing.append(config_obj)
|
||||
|
||||
all_completed = not missing
|
||||
all_missing = not completed
|
||||
|
||||
tools = [tool_type.value.title() for tool_type in status]
|
||||
completed_tools = (
|
||||
", ".join(tools[:-1]) + " and " + tools[-1] if len(tools) > 1 else tools[0]
|
||||
)
|
||||
|
||||
if all_completed:
|
||||
console.print(
|
||||
f"{emoji_check} {completed_tools} {MSG_SETUP_PACKAGE_FIREWALL_RESULT}"
|
||||
)
|
||||
console.print(MSG_SETUP_PACKAGE_FIREWALL_NOTE_STATUS)
|
||||
else:
|
||||
progressive_print([MSG_SETUP_INCOMPLETE])
|
||||
|
||||
console.line()
|
||||
|
||||
emit_firewall_setup_completed(
|
||||
event_bus=ctx.obj.event_bus,
|
||||
ctx=ctx,
|
||||
status=status,
|
||||
)
|
||||
|
||||
return completed_tools, all_completed, all_missing, status
|
||||
@@ -0,0 +1,6 @@
|
||||
from safety_schemas.models.events.payloads import InitExitStep
|
||||
|
||||
|
||||
class StepTracker:
|
||||
def __init__(self):
|
||||
self.current_step: InitExitStep = InitExitStep.UNKNOWN
|
||||
164
Backend/venv/lib/python3.12/site-packages/safety/init/render.py
Normal file
164
Backend/venv/lib/python3.12/site-packages/safety/init/render.py
Normal file
@@ -0,0 +1,164 @@
|
||||
import time
|
||||
from typing import List, Union
|
||||
|
||||
from rich.console import RenderableType
|
||||
from rich.prompt import Prompt
|
||||
import typer
|
||||
from safety.console import main_console as console
|
||||
from safety.events.utils.emission import (
|
||||
emit_codebase_setup_response_created,
|
||||
emit_firewall_setup_response_created,
|
||||
)
|
||||
from safety.init.constants import (
|
||||
MSG_SETUP_CODEBASE_PROMPT,
|
||||
MSG_SETUP_CONTINUE_PROMPT,
|
||||
MSG_SETUP_PACKAGE_FIREWALL_PROMPT,
|
||||
)
|
||||
|
||||
|
||||
def typed_print(
|
||||
text: str, delay: float = 0.02, console=console, style="bold", end_line=True
|
||||
):
|
||||
rich_text = console.render_str(text)
|
||||
text = rich_text.plain
|
||||
|
||||
for char in text:
|
||||
console.print(char, end="", style=style)
|
||||
if char != "\n":
|
||||
time.sleep(delay)
|
||||
if end_line:
|
||||
console.line()
|
||||
|
||||
|
||||
def progressive_print(
|
||||
sections: Union[List[str], List[RenderableType]],
|
||||
pause_between: float = 0.7,
|
||||
console=console,
|
||||
):
|
||||
for section in sections:
|
||||
obj = section
|
||||
|
||||
if isinstance(section, str):
|
||||
obj = console.render_str(section)
|
||||
|
||||
console.print(obj)
|
||||
time.sleep(pause_between)
|
||||
|
||||
|
||||
def render_header(
|
||||
title, emoji=":shield:", margin_left=0, margin_right=2, console=console
|
||||
):
|
||||
"""
|
||||
Create a modern header with emoji that works cross-platform
|
||||
"""
|
||||
content = f"{' ' * margin_left}{emoji}{title}{' ' * margin_right}"
|
||||
rendered_content = console.render_str(content)
|
||||
plain_text = rendered_content.plain
|
||||
|
||||
underline = console.render_str(f"[blue]{'━' * len(plain_text)}[/blue]")
|
||||
|
||||
console.print()
|
||||
typed_print(plain_text, style="bold white", delay=0.01, console=console)
|
||||
console.print(underline)
|
||||
console.print()
|
||||
|
||||
|
||||
def ask_firewall_setup(ctx: typer.Context, prompt_user: bool = True) -> bool:
|
||||
"""
|
||||
Ask the user if they want to set up Safety Firewall.
|
||||
|
||||
As a side effect, this function emits an event with the response.
|
||||
|
||||
Args:
|
||||
ctx: The CLI context
|
||||
prompt_user: Whether to prompt the user for input
|
||||
|
||||
Returns:
|
||||
bool: True if the user wants to set up Safety Firewall, False otherwise
|
||||
"""
|
||||
firewall_choice = "y"
|
||||
|
||||
if prompt_user:
|
||||
firewall_choice = Prompt.ask(
|
||||
MSG_SETUP_PACKAGE_FIREWALL_PROMPT,
|
||||
choices=["y", "n", "Y", "N"],
|
||||
default="y",
|
||||
show_default=False,
|
||||
show_choices=False,
|
||||
console=console,
|
||||
).lower()
|
||||
|
||||
should_setup_firewall = firewall_choice == "y"
|
||||
|
||||
emit_firewall_setup_response_created(
|
||||
event_bus=ctx.obj.event_bus,
|
||||
ctx=ctx,
|
||||
user_consent_requested=prompt_user,
|
||||
user_consent=should_setup_firewall if prompt_user else None,
|
||||
)
|
||||
|
||||
return should_setup_firewall
|
||||
|
||||
|
||||
def ask_codebase_setup(ctx: typer.Context, prompt_user: bool = True) -> bool:
|
||||
"""
|
||||
Ask the user if they want to set up a codebase.
|
||||
|
||||
As a side effect, this function emits an event with the response.
|
||||
|
||||
Args:
|
||||
ctx: The CLI context
|
||||
prompt_user: Whether to prompt the user for input
|
||||
|
||||
Returns:
|
||||
bool: True if the user wants to set up a codebase, False otherwise
|
||||
"""
|
||||
codebase_response = "y"
|
||||
|
||||
if prompt_user:
|
||||
codebase_response = Prompt.ask(
|
||||
MSG_SETUP_CODEBASE_PROMPT,
|
||||
choices=["y", "n", "Y", "N"],
|
||||
default="y",
|
||||
show_default=False,
|
||||
show_choices=False,
|
||||
console=console,
|
||||
).lower()
|
||||
|
||||
should_setup_codebase = codebase_response == "y"
|
||||
|
||||
emit_codebase_setup_response_created(
|
||||
event_bus=ctx.obj.event_bus,
|
||||
ctx=ctx,
|
||||
user_consent_requested=prompt_user,
|
||||
user_consent=should_setup_codebase if prompt_user else None,
|
||||
)
|
||||
|
||||
return should_setup_codebase
|
||||
|
||||
|
||||
def ask_continue(ctx: typer.Context, prompt_user: bool = True) -> bool:
|
||||
"""
|
||||
Ask the user if they want to continue by typing enter
|
||||
|
||||
Args:
|
||||
ctx: The CLI context
|
||||
prompt_user: Whether to prompt the user for input
|
||||
|
||||
Returns:
|
||||
bool: True if the user wants to continue, False otherwise
|
||||
"""
|
||||
if prompt_user:
|
||||
return (
|
||||
Prompt.ask(
|
||||
MSG_SETUP_CONTINUE_PROMPT,
|
||||
choices=["y", "Y"],
|
||||
default="y",
|
||||
show_default=False,
|
||||
show_choices=False,
|
||||
console=console,
|
||||
).lower()
|
||||
== "y"
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,13 @@
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from safety_schemas.models.events.types import ToolType
|
||||
from safety_schemas.models.events.payloads import (
|
||||
AliasConfig,
|
||||
IndexConfig,
|
||||
)
|
||||
|
||||
|
||||
FirewallConfigStatus = Dict[
|
||||
ToolType, Dict[str, Optional[Union[AliasConfig, IndexConfig]]]
|
||||
]
|
||||
Reference in New Issue
Block a user