766 lines
25 KiB
Python
766 lines
25 KiB
Python
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
|