updates
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
from .emission import (
|
||||
emit_command_error,
|
||||
emit_command_executed,
|
||||
emit_firewall_disabled,
|
||||
emit_diff_operations,
|
||||
emit_firewall_configured,
|
||||
emit_tool_command_executed,
|
||||
emit_firewall_heartbeat,
|
||||
emit_init_started,
|
||||
emit_auth_started,
|
||||
emit_auth_completed,
|
||||
)
|
||||
|
||||
from .creation import (
|
||||
create_internal_event,
|
||||
InternalEventType,
|
||||
InternalPayload,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"emit_command_error",
|
||||
"emit_command_executed",
|
||||
"emit_firewall_disabled",
|
||||
"create_internal_event",
|
||||
"InternalEventType",
|
||||
"InternalPayload",
|
||||
"emit_firewall_configured",
|
||||
"emit_diff_operations",
|
||||
"emit_init_started",
|
||||
"emit_auth_started",
|
||||
"emit_auth_completed",
|
||||
"emit_tool_command_executed",
|
||||
"emit_firewall_heartbeat",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
from functools import wraps
|
||||
from typing import TYPE_CHECKING, Any, Callable, List, Optional, TypeVar, cast, overload
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from safety.events.event_bus import EventBus
|
||||
from safety.cli_util import CustomContext
|
||||
|
||||
|
||||
def should_emit(
|
||||
event_bus: Optional["EventBus"], ctx: Optional["CustomContext"]
|
||||
) -> bool:
|
||||
"""
|
||||
Common conditions that apply to all event emissions.
|
||||
"""
|
||||
if event_bus is None:
|
||||
return False
|
||||
|
||||
# Be aware that ctx depends on the command being parsed, if the emit func
|
||||
# is called from the entrypoint group command, ctx will not have
|
||||
# the command parsed yet.
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def should_emit_firewall_heartbeat(ctx: Optional["CustomContext"]) -> bool:
|
||||
"""
|
||||
Condition to check if the firewall is enabled.
|
||||
"""
|
||||
if ctx and ctx.obj.firewall_enabled:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# Define TypeVars for better typing
|
||||
F = TypeVar("F", bound=Callable[..., Any])
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
@overload
|
||||
def conditional_emitter(emit_func: F, *, conditions: None = None) -> F: ...
|
||||
|
||||
|
||||
@overload
|
||||
def conditional_emitter(
|
||||
emit_func: None = None,
|
||||
*,
|
||||
conditions: Optional[List[Callable[[Optional["CustomContext"]], bool]]] = None,
|
||||
) -> Callable[[F], F]: ...
|
||||
|
||||
|
||||
def conditional_emitter(
|
||||
emit_func=None,
|
||||
*,
|
||||
conditions: Optional[List[Callable[[Optional["CustomContext"]], bool]]] = None,
|
||||
):
|
||||
"""
|
||||
A decorator that conditionally calls the decorated function based on conditions.
|
||||
Only executes the decorated function if all conditions evaluate to True.
|
||||
"""
|
||||
|
||||
def decorator(func: F) -> F:
|
||||
@wraps(func)
|
||||
def wrapper(event_bus, ctx=None, *args, **kwargs):
|
||||
if not should_emit(event_bus, ctx):
|
||||
return None
|
||||
|
||||
if conditions:
|
||||
if all(condition(ctx) for condition in conditions):
|
||||
return func(event_bus, ctx, *args, **kwargs)
|
||||
return None
|
||||
return func(event_bus, ctx, *args, **kwargs)
|
||||
|
||||
return cast(F, wrapper) # Cast to help type checker
|
||||
|
||||
if emit_func is None:
|
||||
return decorator
|
||||
return decorator(emit_func)
|
||||
@@ -0,0 +1,163 @@
|
||||
import getpass
|
||||
import os
|
||||
from pathlib import Path
|
||||
import site
|
||||
import socket
|
||||
import sys
|
||||
import platform
|
||||
from typing import List, Optional
|
||||
from safety_schemas.models.events.context import (
|
||||
ClientInfo,
|
||||
EventContext,
|
||||
HostInfo,
|
||||
OsInfo,
|
||||
ProjectInfo,
|
||||
PythonInfo,
|
||||
RuntimeInfo,
|
||||
UserInfo,
|
||||
)
|
||||
|
||||
from safety_schemas.models.events.types import SourceType
|
||||
from safety_schemas.models import ProjectModel
|
||||
|
||||
|
||||
def get_user_info() -> UserInfo:
|
||||
"""
|
||||
Collect information about the current user.
|
||||
"""
|
||||
return UserInfo(name=getpass.getuser(), home_dir=str(Path.home()))
|
||||
|
||||
|
||||
def get_os_info() -> OsInfo:
|
||||
"""
|
||||
Get basic OS information using only the platform module.
|
||||
Returns a dictionary with architecture, platform, name, version, and kernel_version.
|
||||
"""
|
||||
# Initialize with required fields
|
||||
os_info = {
|
||||
"architecture": platform.machine(),
|
||||
"platform": platform.system(),
|
||||
"name": None,
|
||||
"version": None,
|
||||
"kernel_version": None,
|
||||
}
|
||||
|
||||
python_version = sys.version_info
|
||||
|
||||
if sys.platform == "wind32":
|
||||
os_info["version"] = platform.release()
|
||||
os_info["kernel_version"] = platform.version()
|
||||
os_info["name"] = "windows"
|
||||
|
||||
elif sys.platform == "darwin":
|
||||
os_info["version"] = platform.mac_ver()[0]
|
||||
os_info["kernel_version"] = platform.release()
|
||||
os_info["name"] = "macos"
|
||||
|
||||
elif sys.platform == "linux":
|
||||
os_info["kernel_version"] = platform.release()
|
||||
if python_version >= (3, 10):
|
||||
try:
|
||||
os_release = platform.freedesktop_os_release()
|
||||
# Use ID for name (more consistent for programmatic use)
|
||||
os_info["name"] = os_release.get("ID", "linux")
|
||||
os_info["version"] = os_release.get("VERSION_ID")
|
||||
except (OSError, AttributeError):
|
||||
# If freedesktop_os_release fails, keep values as is
|
||||
pass
|
||||
|
||||
return OsInfo(**os_info)
|
||||
|
||||
|
||||
def get_host_info() -> HostInfo:
|
||||
"""
|
||||
Collect information about the host machine.
|
||||
"""
|
||||
hostname = socket.gethostname()
|
||||
|
||||
ipv4_addresses = set()
|
||||
ipv6_addresses = set()
|
||||
try:
|
||||
host_info = socket.getaddrinfo(hostname, None)
|
||||
for info in host_info:
|
||||
ip_family = info[0]
|
||||
ip = str(info[4][0])
|
||||
|
||||
if ip_family == socket.AF_INET:
|
||||
if not ip.startswith("127."):
|
||||
ipv4_addresses.add(ip)
|
||||
elif ip_family == socket.AF_INET6:
|
||||
if not ip.startswith("::1") and ip != "fe80::1":
|
||||
ipv6_addresses.add(ip)
|
||||
|
||||
# Prioritize addresses
|
||||
primary_ipv4 = next(
|
||||
(ip for ip in ipv4_addresses),
|
||||
next(iter(ipv4_addresses)) if ipv4_addresses else None,
|
||||
)
|
||||
|
||||
primary_ipv6 = next(
|
||||
(ip for ip in ipv6_addresses if not ip.startswith("fe80:")),
|
||||
next(iter(ipv6_addresses)) if ipv6_addresses else None,
|
||||
)
|
||||
|
||||
except socket.gaierror:
|
||||
primary_ipv4 = None
|
||||
primary_ipv6 = None
|
||||
|
||||
return HostInfo(name=hostname, ipv4=primary_ipv4, ipv6=primary_ipv6, timezone=None)
|
||||
|
||||
|
||||
def get_python_info() -> PythonInfo:
|
||||
"""
|
||||
Collect detailed information about the Python environment.
|
||||
"""
|
||||
# Get site-packages directories
|
||||
site_packages_dirs = site.getsitepackages()
|
||||
|
||||
user_site_enabled = bool(site.ENABLE_USER_SITE)
|
||||
user_site_packages = site.getusersitepackages()
|
||||
|
||||
return PythonInfo(
|
||||
version=f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||
path=sys.executable,
|
||||
sys_path=sys.path,
|
||||
implementation=platform.python_implementation(),
|
||||
implementation_version=platform.python_version(),
|
||||
sys_prefix=sys.prefix,
|
||||
site_packages=site_packages_dirs,
|
||||
user_site_enabled=user_site_enabled,
|
||||
user_site_packages=user_site_packages,
|
||||
encoding=sys.getdefaultencoding(),
|
||||
filesystem_encoding=sys.getfilesystemencoding(),
|
||||
)
|
||||
|
||||
|
||||
def create_event_context(
|
||||
client_identifier: SourceType,
|
||||
client_version: str,
|
||||
client_path: str,
|
||||
project: Optional[ProjectModel] = None,
|
||||
tags: Optional[List[str]] = None,
|
||||
) -> EventContext:
|
||||
client = ClientInfo(
|
||||
identifier=client_identifier, version=client_version, path=client_path
|
||||
)
|
||||
|
||||
project_info = None
|
||||
|
||||
if project:
|
||||
project_info = ProjectInfo(
|
||||
id=project.id,
|
||||
url=project.url_path,
|
||||
)
|
||||
|
||||
runtime = RuntimeInfo(
|
||||
workdir=os.getcwd(),
|
||||
user=get_user_info(),
|
||||
os=get_os_info(),
|
||||
host=get_host_info(),
|
||||
python=get_python_info(),
|
||||
)
|
||||
|
||||
return EventContext(client=client, runtime=runtime, project=project_info, tags=tags)
|
||||
@@ -0,0 +1,48 @@
|
||||
import time
|
||||
from typing import Optional, TypeVar
|
||||
|
||||
from safety_schemas.models.events import Event, EventTypeBase, PayloadBase, SourceType
|
||||
|
||||
from safety.meta import get_identifier
|
||||
|
||||
from ..types import InternalEventType, InternalPayload
|
||||
|
||||
PayloadBaseT = TypeVar("PayloadBaseT", bound=PayloadBase)
|
||||
EventTypeBaseT = TypeVar("EventTypeBaseT", bound=EventTypeBase)
|
||||
|
||||
|
||||
def create_event(
|
||||
payload: PayloadBaseT,
|
||||
event_type: EventTypeBaseT,
|
||||
source: SourceType = SourceType(get_identifier()),
|
||||
timestamp: int = int(time.time()),
|
||||
correlation_id: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Event[EventTypeBaseT, PayloadBaseT]:
|
||||
"""
|
||||
Generic factory function for creating any type of event.
|
||||
"""
|
||||
|
||||
return Event(
|
||||
timestamp=timestamp,
|
||||
payload=payload,
|
||||
type=event_type,
|
||||
source=source,
|
||||
correlation_id=correlation_id,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def create_internal_event(
|
||||
event_type: InternalEventType,
|
||||
payload: InternalPayload,
|
||||
) -> Event[InternalEventType, InternalPayload]:
|
||||
"""
|
||||
Create an internal event.
|
||||
"""
|
||||
return Event(
|
||||
type=event_type,
|
||||
timestamp=int(time.time()),
|
||||
source=SourceType(get_identifier()),
|
||||
payload=payload,
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
import re
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from click.core import ParameterSource as ClickParameterSource
|
||||
|
||||
from safety_schemas.models.events.types import ParamSource
|
||||
|
||||
|
||||
def is_sensitive_parameter(param_name: str) -> bool:
|
||||
"""
|
||||
Determine if a parameter name likely contains sensitive information.
|
||||
"""
|
||||
sensitive_patterns = [
|
||||
r"(?i)pass(word)?", # password, pass
|
||||
r"(?i)token", # token, auth_token
|
||||
r"(?i)key", # key, apikey
|
||||
r"(?i)auth", # auth, authorization
|
||||
]
|
||||
|
||||
return any(re.search(pattern, param_name) for pattern in sensitive_patterns)
|
||||
|
||||
|
||||
def scrub_sensitive_value(value: str) -> str:
|
||||
"""
|
||||
Detect if a value appears to be sensitive information based on
|
||||
specific patterns.
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
|
||||
result = value
|
||||
|
||||
if re.match(r"^-{1,2}[\w-]+$", value) and "=" not in value:
|
||||
return value
|
||||
|
||||
# Patterns to detect and replace
|
||||
patterns = [
|
||||
# This will replace ports too, but that's fine
|
||||
(r"\b\w+:\w+\b", "-:-"),
|
||||
(r"Basic\s+[A-Za-z0-9+/=]+", "Basic -"),
|
||||
(r"Bearer\s+[A-Za-z0-9._~+/=-]+", "Bearer -"),
|
||||
(r"\b[A-Za-z0-9_-]{20,}\b", "-"),
|
||||
(
|
||||
r"((?:token|api|apikey|key|auth|secret|password|access|jwt|bearer|credential|pwd)=)([^&\s]+)",
|
||||
r"\1-",
|
||||
),
|
||||
]
|
||||
|
||||
# Apply each pattern and replace matches
|
||||
for pattern, repl in patterns:
|
||||
result = re.sub(pattern, repl, result)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def clean_parameter(param_name: str, param_value: Any) -> Any:
|
||||
"""
|
||||
Scrub a parameter value if it's sensitive.
|
||||
"""
|
||||
if not isinstance(param_value, str):
|
||||
return param_value
|
||||
|
||||
if is_sensitive_parameter(param_name):
|
||||
return "-"
|
||||
|
||||
return scrub_sensitive_value(param_value)
|
||||
|
||||
|
||||
def get_command_path(ctx) -> List[str]:
|
||||
hierarchy = []
|
||||
current = ctx
|
||||
|
||||
while current is not None:
|
||||
if current.command:
|
||||
name = current.command.name
|
||||
if name == "cli":
|
||||
name = "safety"
|
||||
hierarchy.append(name)
|
||||
current = current.parent
|
||||
|
||||
# Reverse to get top-level first
|
||||
hierarchy.reverse()
|
||||
|
||||
return hierarchy
|
||||
|
||||
|
||||
def get_root_context(ctx):
|
||||
"""
|
||||
Get the top-level parent context.
|
||||
"""
|
||||
current = ctx
|
||||
while current.parent is not None:
|
||||
current = current.parent
|
||||
return current
|
||||
|
||||
|
||||
def translate_param_source(source: Optional[ClickParameterSource]) -> ParamSource:
|
||||
"""
|
||||
Translate Click's ParameterSource enum to our ParameterSource enum
|
||||
"""
|
||||
mapping = {
|
||||
ClickParameterSource.COMMANDLINE: ParamSource.COMMANDLINE,
|
||||
ClickParameterSource.ENVIRONMENT: ParamSource.ENVIRONMENT,
|
||||
ClickParameterSource.DEFAULT: ParamSource.DEFAULT,
|
||||
# In newer Click versions
|
||||
getattr(ClickParameterSource, "PROMPT", None): ParamSource.PROMPT,
|
||||
getattr(ClickParameterSource, "CONFIG_FILE", None): ParamSource.CONFIG,
|
||||
}
|
||||
|
||||
return mapping.get(source, ParamSource.UNKNOWN)
|
||||
@@ -0,0 +1,681 @@
|
||||
from concurrent.futures import Future
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
import uuid
|
||||
|
||||
from safety.utils.pyapp_utils import get_path, get_env
|
||||
|
||||
from safety_schemas.models.events import Event, EventType
|
||||
from safety_schemas.models.events.types import ToolType
|
||||
from safety_schemas.models.events.payloads import (
|
||||
CodebaseDetectionStatusPayload,
|
||||
CodebaseSetupCompletedPayload,
|
||||
CodebaseSetupResponseCreatedPayload,
|
||||
DependencyFile,
|
||||
FirewallConfiguredPayload,
|
||||
FirewallDisabledPayload,
|
||||
FirewallSetupCompletedPayload,
|
||||
FirewallSetupResponseCreatedPayload,
|
||||
InitExitStep,
|
||||
InitExitedPayload,
|
||||
InitScanCompletedPayload,
|
||||
PackageInstalledPayload,
|
||||
PackageUninstalledPayload,
|
||||
PackageUpdatedPayload,
|
||||
CommandExecutedPayload,
|
||||
ToolCommandExecutedPayload,
|
||||
CommandErrorPayload,
|
||||
AliasConfig,
|
||||
IndexConfig,
|
||||
ToolStatus,
|
||||
CommandParam,
|
||||
ProcessStatus,
|
||||
FirewallHeartbeatPayload,
|
||||
InitStartedPayload,
|
||||
AuthStartedPayload,
|
||||
AuthCompletedPayload,
|
||||
)
|
||||
import typer
|
||||
|
||||
from ..event_bus import EventBus
|
||||
from ..types.base import InternalEventType, InternalPayload
|
||||
|
||||
from .creation import (
|
||||
create_event,
|
||||
)
|
||||
from .data import (
|
||||
clean_parameter,
|
||||
get_command_path,
|
||||
get_root_context,
|
||||
scrub_sensitive_value,
|
||||
translate_param_source,
|
||||
)
|
||||
from .conditions import conditional_emitter, should_emit_firewall_heartbeat
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from safety.models import SafetyCLI, ToolResult
|
||||
from safety.cli_util import CustomContext
|
||||
from safety.init.types import FirewallConfigStatus
|
||||
from safety.tool.environment_diff import PackageLocation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def send_and_flush(event_bus: "EventBus", event: Event) -> Optional[Future]:
|
||||
"""
|
||||
Emit an event and immediately flush the event bus without closing it.
|
||||
|
||||
Args:
|
||||
event_bus: The event bus to emit on
|
||||
event: The event to emit
|
||||
"""
|
||||
future = event_bus.emit(event)
|
||||
|
||||
# Create and emit flush event
|
||||
flush_payload = InternalPayload()
|
||||
flush_event = create_event(
|
||||
payload=flush_payload, event_type=InternalEventType.FLUSH_SECURITY_TRACES
|
||||
)
|
||||
|
||||
# Emit flush event and wait for it to complete
|
||||
flush_future = event_bus.emit(flush_event)
|
||||
|
||||
# Wait for both events to complete
|
||||
if future:
|
||||
try:
|
||||
future.result(timeout=0.5)
|
||||
except Exception:
|
||||
logger.error("Emit Failed %s (%s)", event.type, event.id)
|
||||
|
||||
if flush_future:
|
||||
try:
|
||||
return flush_future.result(timeout=0.5)
|
||||
except Exception:
|
||||
logger.error("Flush Failed for event %s", event.id)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@conditional_emitter(conditions=[should_emit_firewall_heartbeat])
|
||||
def emit_firewall_heartbeat(
|
||||
event_bus: "EventBus", ctx: Optional["CustomContext"], *, tools: List[ToolStatus]
|
||||
):
|
||||
payload = FirewallHeartbeatPayload(tools=tools)
|
||||
event = create_event(payload=payload, event_type=EventType.FIREWALL_HEARTBEAT)
|
||||
|
||||
event_bus.emit(event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_firewall_disabled(
|
||||
event_bus: "EventBus",
|
||||
ctx: Optional["CustomContext"] = None,
|
||||
*,
|
||||
reason: Optional[str],
|
||||
):
|
||||
payload = FirewallDisabledPayload(reason=reason)
|
||||
event = create_event(payload=payload, event_type=EventType.FIREWALL_DISABLED)
|
||||
|
||||
event_bus.emit(event)
|
||||
|
||||
|
||||
def status_to_tool_status(status: "FirewallConfigStatus") -> List[ToolStatus]:
|
||||
filtered_path = get_path()
|
||||
tools = []
|
||||
for tool_type, configs in status.items():
|
||||
alias_config = (
|
||||
configs["alias"] if isinstance(configs["alias"], AliasConfig) else None
|
||||
)
|
||||
index_config = (
|
||||
configs["index"] if isinstance(configs["index"], IndexConfig) else None
|
||||
)
|
||||
|
||||
tool = tool_type.value
|
||||
command_path = shutil.which(tool, path=filtered_path)
|
||||
reachable = False
|
||||
version = "unknown"
|
||||
|
||||
if command_path:
|
||||
args = [command_path, "--version"]
|
||||
result = subprocess.run(args, capture_output=True, text=True, env=get_env())
|
||||
|
||||
if result.returncode == 0:
|
||||
output = result.stdout
|
||||
reachable = True
|
||||
|
||||
# Extract version
|
||||
version_match = re.search(r"(\d+\.\d+(?:\.\d+)?)", output)
|
||||
if version_match:
|
||||
version = version_match.group(1)
|
||||
else:
|
||||
command_path = tool
|
||||
|
||||
tool = ToolStatus(
|
||||
type=tool_type,
|
||||
command_path=command_path,
|
||||
version=version,
|
||||
reachable=reachable,
|
||||
alias_config=alias_config,
|
||||
index_config=index_config,
|
||||
)
|
||||
tools.append(tool)
|
||||
|
||||
return tools
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_firewall_configured(
|
||||
event_bus: "EventBus",
|
||||
ctx: Optional["CustomContext"] = None,
|
||||
*,
|
||||
status: "FirewallConfigStatus",
|
||||
):
|
||||
tools = status_to_tool_status(status)
|
||||
|
||||
payload = FirewallConfiguredPayload(tools=tools)
|
||||
|
||||
event = create_event(payload=payload, event_type=EventType.FIREWALL_CONFIGURED)
|
||||
|
||||
event_bus.emit(event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_diff_operations(
|
||||
event_bus: "EventBus",
|
||||
ctx: "CustomContext",
|
||||
*,
|
||||
added: Dict["PackageLocation", str],
|
||||
removed: Dict["PackageLocation", str],
|
||||
updated: Dict["PackageLocation", Tuple[str, str]],
|
||||
tool_path: Optional[str],
|
||||
by_tool: ToolType,
|
||||
):
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
correlation_id = obj.correlation_id
|
||||
|
||||
kwargs = {
|
||||
"tool_path": tool_path,
|
||||
"tool": by_tool,
|
||||
}
|
||||
|
||||
if (added or removed or updated) and not correlation_id:
|
||||
correlation_id = obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
def emit_package_event(event_bus, correlation_id, payload, event_type):
|
||||
event = create_event(
|
||||
payload=payload,
|
||||
event_type=event_type,
|
||||
correlation_id=correlation_id,
|
||||
)
|
||||
event_bus.emit(event)
|
||||
|
||||
for package, version in added.items():
|
||||
emit_package_event(
|
||||
event_bus,
|
||||
correlation_id,
|
||||
PackageInstalledPayload(
|
||||
package_name=package.name,
|
||||
location=package.location,
|
||||
version=version,
|
||||
**kwargs,
|
||||
),
|
||||
EventType.PACKAGE_INSTALLED,
|
||||
)
|
||||
|
||||
for package, version in removed.items():
|
||||
emit_package_event(
|
||||
event_bus,
|
||||
correlation_id,
|
||||
PackageUninstalledPayload(
|
||||
package_name=package.name,
|
||||
location=package.location,
|
||||
version=version,
|
||||
**kwargs,
|
||||
),
|
||||
EventType.PACKAGE_UNINSTALLED,
|
||||
)
|
||||
|
||||
for package, (previous_version, current_version) in updated.items():
|
||||
emit_package_event(
|
||||
event_bus,
|
||||
correlation_id,
|
||||
PackageUpdatedPayload(
|
||||
package_name=package.name,
|
||||
location=package.location,
|
||||
previous_version=previous_version,
|
||||
current_version=current_version,
|
||||
**kwargs,
|
||||
),
|
||||
EventType.PACKAGE_UPDATED,
|
||||
)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_tool_command_executed(
|
||||
event_bus: "EventBus", ctx: "CustomContext", *, tool: ToolType, result: "ToolResult"
|
||||
) -> None:
|
||||
correlation_id = ctx.obj.correlation_id
|
||||
|
||||
if not correlation_id:
|
||||
correlation_id = ctx.obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
process = result.process
|
||||
|
||||
payload = ToolCommandExecutedPayload(
|
||||
tool=tool,
|
||||
tool_path=result.tool_path,
|
||||
raw_command=[clean_parameter("", arg) for arg in process.args],
|
||||
duration_ms=result.duration_ms,
|
||||
status=ProcessStatus(
|
||||
stdout=process.stdout, stderr=process.stderr, return_code=process.returncode
|
||||
),
|
||||
)
|
||||
|
||||
# Scrub after binary coercion to str
|
||||
if payload.status.stdout:
|
||||
payload.status.stdout = scrub_sensitive_value(payload.status.stdout)
|
||||
if payload.status.stderr:
|
||||
payload.status.stderr = scrub_sensitive_value(payload.status.stderr)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.TOOL_COMMAND_EXECUTED,
|
||||
)
|
||||
|
||||
event_bus.emit(event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_command_executed(
|
||||
event_bus: "EventBus", ctx: "CustomContext", *, returned_code: int
|
||||
) -> None:
|
||||
root_context = get_root_context(ctx)
|
||||
NA = ""
|
||||
|
||||
started_at = getattr(root_context, "started_at", None) if root_context else None
|
||||
if started_at is not None:
|
||||
duration_ms = int((time.monotonic() - started_at) * 1000)
|
||||
else:
|
||||
duration_ms = 1
|
||||
|
||||
command_name = ctx.command.name if ctx.command.name is not None else NA
|
||||
raw_command = [clean_parameter("", arg) for arg in sys.argv]
|
||||
|
||||
params: List[CommandParam] = []
|
||||
|
||||
for idx, param in enumerate(ctx.command.params):
|
||||
param_name = param.name if param.name is not None else NA
|
||||
param_value = ctx.params.get(param_name)
|
||||
|
||||
# Scrub the parameter value if sensitive
|
||||
scrubbed_value = clean_parameter(param_name, param_value)
|
||||
|
||||
# Determine parameter source using Click's API
|
||||
click_source = ctx.get_parameter_source(param_name)
|
||||
source = translate_param_source(click_source)
|
||||
|
||||
display_name = param_name if param_name else None
|
||||
|
||||
params.append(
|
||||
CommandParam(
|
||||
position=idx, name=display_name, value=scrubbed_value, source=source
|
||||
)
|
||||
)
|
||||
|
||||
payload = CommandExecutedPayload(
|
||||
command_name=command_name,
|
||||
command_path=get_command_path(ctx),
|
||||
raw_command=raw_command,
|
||||
parameters=params,
|
||||
duration_ms=duration_ms,
|
||||
status=ProcessStatus(
|
||||
return_code=returned_code,
|
||||
),
|
||||
)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=ctx.obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.COMMAND_EXECUTED,
|
||||
)
|
||||
|
||||
try:
|
||||
if future := event_bus.emit(event):
|
||||
future.result(timeout=0.5)
|
||||
except Exception:
|
||||
logger.error("Emit Failed %s (%s)", event.type, event.id)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_command_error(
|
||||
event_bus: "EventBus",
|
||||
ctx: "CustomContext",
|
||||
*,
|
||||
message: str,
|
||||
traceback: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Emit a CommandErrorEvent with sensitive data scrubbed.
|
||||
"""
|
||||
# Get command name from context if available
|
||||
command_name = getattr(ctx, "command", None)
|
||||
if command_name and command_name.name:
|
||||
command_name = command_name.name
|
||||
|
||||
scrub_traceback = None
|
||||
if traceback:
|
||||
scrub_traceback = scrub_sensitive_value(traceback)
|
||||
|
||||
command_path = get_command_path(ctx)
|
||||
raw_command = [scrub_sensitive_value(arg) for arg in sys.argv]
|
||||
|
||||
payload = CommandErrorPayload(
|
||||
command_name=command_name,
|
||||
raw_command=raw_command,
|
||||
command_path=command_path,
|
||||
error_message=scrub_sensitive_value(message),
|
||||
stacktrace=scrub_traceback,
|
||||
)
|
||||
|
||||
event = create_event(
|
||||
payload=payload,
|
||||
event_type=EventType.COMMAND_ERROR,
|
||||
)
|
||||
|
||||
event_bus.emit(event)
|
||||
|
||||
|
||||
def emit_init_started(
|
||||
event_bus: "EventBus", ctx: Union["CustomContext", typer.Context]
|
||||
) -> None:
|
||||
"""
|
||||
Emit an InitStartedEvent and store it as a pending event in SafetyCLI object.
|
||||
|
||||
Args:
|
||||
event_bus: The event bus to emit on
|
||||
ctx: The Click context containing the SafetyCLI object
|
||||
"""
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = InitStartedPayload()
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.INIT_STARTED,
|
||||
)
|
||||
|
||||
if not send_and_flush(event_bus, event):
|
||||
# Store as pending event
|
||||
obj.pending_events.append(event)
|
||||
|
||||
|
||||
def emit_auth_started(event_bus: "EventBus", ctx: "CustomContext") -> None:
|
||||
"""
|
||||
Emit an AuthStartedEvent and store it as a pending event in SafetyCLI object.
|
||||
|
||||
Args:
|
||||
event_bus: The event bus to emit on
|
||||
ctx: The Click context containing the SafetyCLI object
|
||||
"""
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = AuthStartedPayload()
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.AUTH_STARTED,
|
||||
)
|
||||
|
||||
if not send_and_flush(event_bus, event):
|
||||
# Store as pending event
|
||||
obj.pending_events.append(event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_auth_completed(
|
||||
event_bus: "EventBus",
|
||||
ctx: "CustomContext",
|
||||
*,
|
||||
success: bool = True,
|
||||
error_message: Optional[str] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Emit an AuthCompletedEvent and submit all pending events together.
|
||||
|
||||
Args:
|
||||
event_bus: The event bus to emit on
|
||||
ctx: The Click context containing the SafetyCLI object
|
||||
success: Whether authentication was successful
|
||||
error_message: Optional error message if authentication failed
|
||||
"""
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = AuthCompletedPayload(success=success, error_message=error_message)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.AUTH_COMPLETED,
|
||||
)
|
||||
|
||||
for pending_event in obj.pending_events:
|
||||
event_bus.emit(pending_event)
|
||||
|
||||
obj.pending_events.clear()
|
||||
|
||||
# Emit auth completed event and flush
|
||||
send_and_flush(event_bus, event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_firewall_setup_response_created(
|
||||
event_bus: "EventBus",
|
||||
ctx: Union["CustomContext", typer.Context],
|
||||
*,
|
||||
user_consent_requested: bool,
|
||||
user_consent: Optional[bool] = None,
|
||||
) -> None:
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = FirewallSetupResponseCreatedPayload(
|
||||
user_consent_requested=user_consent_requested, user_consent=user_consent
|
||||
)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.FIREWALL_SETUP_RESPONSE_CREATED,
|
||||
)
|
||||
|
||||
# Emit and flush
|
||||
send_and_flush(event_bus, event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_codebase_setup_response_created(
|
||||
event_bus: "EventBus",
|
||||
ctx: Union["CustomContext", typer.Context],
|
||||
*,
|
||||
user_consent_requested: bool,
|
||||
user_consent: Optional[bool] = None,
|
||||
) -> None:
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = CodebaseSetupResponseCreatedPayload(
|
||||
user_consent_requested=user_consent_requested, user_consent=user_consent
|
||||
)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.CODEBASE_SETUP_RESPONSE_CREATED,
|
||||
)
|
||||
|
||||
# Emit and flush
|
||||
send_and_flush(event_bus, event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_codebase_detection_status(
|
||||
event_bus: "EventBus",
|
||||
ctx: Union["CustomContext", typer.Context],
|
||||
*,
|
||||
detected: bool,
|
||||
detected_files: Optional[List[Path]] = None,
|
||||
) -> None:
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = CodebaseDetectionStatusPayload(
|
||||
detected=detected,
|
||||
dependency_files=[
|
||||
DependencyFile(file_path=str(file)) for file in detected_files
|
||||
]
|
||||
if detected_files
|
||||
else None,
|
||||
)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.CODEBASE_DETECTION_STATUS,
|
||||
)
|
||||
|
||||
# Emit and flush
|
||||
send_and_flush(event_bus, event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_init_scan_completed(
|
||||
event_bus: "EventBus",
|
||||
ctx: Union["CustomContext", typer.Context],
|
||||
*,
|
||||
scan_id: Optional[str],
|
||||
) -> None:
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = InitScanCompletedPayload(scan_id=scan_id)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.INIT_SCAN_COMPLETED,
|
||||
)
|
||||
|
||||
# Emit and flush
|
||||
send_and_flush(event_bus, event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_codebase_setup_completed(
|
||||
event_bus: "EventBus",
|
||||
ctx: Union["CustomContext", typer.Context],
|
||||
*,
|
||||
is_created: bool,
|
||||
codebase_id: Optional[str] = None,
|
||||
) -> None:
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = CodebaseSetupCompletedPayload(
|
||||
is_created=is_created, codebase_id=codebase_id
|
||||
)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.CODEBASE_SETUP_COMPLETED,
|
||||
)
|
||||
|
||||
# Emit and flush
|
||||
send_and_flush(event_bus, event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_firewall_setup_completed(
|
||||
event_bus: "EventBus",
|
||||
ctx: "CustomContext",
|
||||
*,
|
||||
status: "FirewallConfigStatus",
|
||||
) -> None:
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
tools = status_to_tool_status(status)
|
||||
|
||||
payload = FirewallSetupCompletedPayload(
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.FIREWALL_SETUP_COMPLETED,
|
||||
)
|
||||
|
||||
# Emit and flush
|
||||
send_and_flush(event_bus, event)
|
||||
|
||||
|
||||
@conditional_emitter
|
||||
def emit_init_exited(
|
||||
event_bus: "EventBus",
|
||||
ctx: Union["CustomContext", typer.Context],
|
||||
*,
|
||||
exit_step: InitExitStep,
|
||||
) -> None:
|
||||
obj: "SafetyCLI" = ctx.obj
|
||||
|
||||
if not obj.correlation_id:
|
||||
obj.correlation_id = str(uuid.uuid4())
|
||||
|
||||
payload = InitExitedPayload(exit_step=exit_step)
|
||||
|
||||
event = create_event(
|
||||
correlation_id=obj.correlation_id,
|
||||
payload=payload,
|
||||
event_type=EventType.INIT_EXITED,
|
||||
)
|
||||
|
||||
# Emit and flush
|
||||
send_and_flush(event_bus, event)
|
||||
Reference in New Issue
Block a user