Skip to content

Commit b8ad735

Browse files
committed
feat: add security events for firewall users
1 parent d3d2faf commit b8ad735

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+4043
-1086
lines changed

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,10 @@ dependencies = [
4343
"psutil~=6.1.0",
4444
"pydantic>=2.6.0,<2.10.0",
4545
"requests", # TODO: To be replaced by httpx or aiohttp
46+
"httpx",
47+
"tenacity",
4648
"ruamel.yaml>=0.17.21",
47-
"safety-schemas==0.0.11",
49+
"safety-schemas==0.0.12",
4850
# TODO: To be removed after migrate away from pkg_resources
4951
"setuptools>=65.5.1",
5052
"typer>=0.12.1",
@@ -235,7 +237,7 @@ style = [
235237
]
236238
fmt = [
237239
"ruff format {args:.}",
238-
"ruff --fix {args:.}",
240+
"ruff check --fix {args:.}",
239241
"style",
240242
]
241243
all = [

safety/auth/cli.py

Lines changed: 108 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from safety.console import main_console as console
88
from safety.constants import MSG_FINISH_REGISTRATION_TPL, MSG_VERIFICATION_HINT
99
from safety.meta import get_version
10+
from safety.decorators import notify
1011

1112
try:
1213
from typing import Annotated
@@ -27,6 +28,7 @@
2728
get_token,
2829
)
2930
from safety.auth.server import process_browser_callback
31+
from safety.util import initialize_event_bus
3032
from safety.scan.constants import (
3133
CLI_AUTH_COMMAND_HELP,
3234
CLI_AUTH_HEADLESS_HELP,
@@ -37,6 +39,7 @@
3739
)
3840

3941
from ..cli_util import SafetyCLISubGroup, get_command_for, pass_safety_cli_obj
42+
from safety.error_handlers import handle_cmd_exception
4043
from .constants import (
4144
MSG_FAIL_LOGIN_AUTHED,
4245
MSG_FAIL_REGISTER_AUTHED,
@@ -50,19 +53,20 @@
5053
auth_app = Typer(rich_markup_mode="rich", name="auth")
5154

5255

53-
5456
CMD_LOGIN_NAME = "login"
5557
CMD_REGISTER_NAME = "register"
5658
CMD_STATUS_NAME = "status"
5759
CMD_LOGOUT_NAME = "logout"
5860
DEFAULT_CMD = CMD_LOGIN_NAME
5961

60-
@auth_app.callback(invoke_without_command=True,
61-
cls=SafetyCLISubGroup,
62-
help=CLI_AUTH_COMMAND_HELP,
63-
epilog=DEFAULT_EPILOG,
64-
context_settings={"allow_extra_args": True,
65-
"ignore_unknown_options": True})
62+
63+
@auth_app.callback(
64+
invoke_without_command=True,
65+
cls=SafetyCLISubGroup,
66+
help=CLI_AUTH_COMMAND_HELP,
67+
epilog=DEFAULT_EPILOG,
68+
context_settings={"allow_extra_args": True, "ignore_unknown_options": True},
69+
)
6670
@pass_safety_cli_obj
6771
def auth(ctx: typer.Context) -> None:
6872
"""
@@ -71,12 +75,11 @@ def auth(ctx: typer.Context) -> None:
7175
Args:
7276
ctx (typer.Context): The Typer context object.
7377
"""
74-
LOG.info('auth started')
78+
LOG.info("auth started")
7579

7680
# If no subcommand is invoked, forward to the default command
7781
if not ctx.invoked_subcommand:
78-
default_command = get_command_for(name=DEFAULT_CMD,
79-
typer_instance=auth_app)
82+
default_command = get_command_for(name=DEFAULT_CMD, typer_instance=auth_app)
8083
return ctx.forward(default_command)
8184

8285

@@ -99,6 +102,7 @@ def fail_if_authenticated(ctx: typer.Context, with_msg: str) -> None:
99102
console.print(with_msg.format(email=email))
100103
sys.exit(0)
101104

105+
102106
def render_email_note(auth: Auth) -> str:
103107
"""
104108
Renders a note indicating whether email verification is required.
@@ -111,8 +115,8 @@ def render_email_note(auth: Auth) -> str:
111115
"""
112116
return "" if auth.email_verified else "[red](email verification required)[/red]"
113117

114-
def render_successful_login(auth: Auth,
115-
organization: Optional[str] = None) -> None:
118+
119+
def render_successful_login(auth: Auth, organization: Optional[str] = None) -> None:
116120
"""
117121
Renders a message indicating a successful login.
118122
@@ -132,34 +136,34 @@ def render_successful_login(auth: Auth,
132136
details = [f"[green][bold]Account:[/bold] {email}[/green] {email_note}"]
133137

134138
if organization:
135-
details.insert(0,
136-
"[green][bold]Organization:[/bold] " \
137-
f"{organization}[green]")
139+
details.insert(0, f"[green][bold]Organization:[/bold] {organization}[green]")
138140

139141
for msg in details:
140142
console.print(Padding(msg, (0, 0, 0, 1)), emoji=True)
141143

142144

143145
@auth_app.command(name=CMD_LOGIN_NAME, help=CLI_AUTH_LOGIN_HELP)
146+
@handle_cmd_exception
147+
@notify
144148
def login(
145149
ctx: typer.Context,
146150
headless: Annotated[
147151
Optional[bool],
148152
typer.Option(
149153
"--headless",
150154
help=CLI_AUTH_HEADLESS_HELP,
151-
)
152-
] = None
155+
),
156+
] = None,
153157
) -> None:
154-
headless = headless is True
158+
headless = headless is True
155159
"""
156160
Authenticate Safety CLI with your safetycli.com account using your default browser.
157161
158162
Args:
159163
ctx (typer.Context): The Typer context object.
160164
headless (bool): Whether to run in headless mode.
161165
"""
162-
LOG.info('login started')
166+
LOG.info("login started")
163167

164168
# Check if the user is already authenticated
165169
fail_if_authenticated(ctx, with_msg=MSG_FAIL_LOGIN_AUTHED)
@@ -168,26 +172,33 @@ def login(
168172

169173
info = None
170174

171-
brief_msg: str = "Redirecting your browser to log in; once authenticated, " \
172-
"return here to start using Safety"
175+
brief_msg: str = (
176+
"Redirecting your browser to log in; once authenticated, "
177+
"return here to start using Safety"
178+
)
173179

174180
if ctx.obj.auth.org:
175-
console.print(f"Logging into [bold]{ctx.obj.auth.org.name}[/bold] " \
176-
"organization.")
181+
console.print(
182+
f"Logging into [bold]{ctx.obj.auth.org.name}[/bold] organization."
183+
)
177184

178185
if headless:
179186
brief_msg = "Running in headless mode. Please copy and open the following URL in a browser"
180187

181188
# Get authorization data and generate the authorization URL
182-
uri, initial_state = get_authorization_data(client=ctx.obj.auth.client,
183-
code_verifier=ctx.obj.auth.code_verifier,
184-
organization=ctx.obj.auth.org, headless=headless)
189+
uri, initial_state = get_authorization_data(
190+
client=ctx.obj.auth.client,
191+
code_verifier=ctx.obj.auth.code_verifier,
192+
organization=ctx.obj.auth.org,
193+
headless=headless,
194+
)
185195
click.secho(brief_msg)
186196
click.echo()
187197

188198
# Process the browser callback to complete the authentication
189-
info = process_browser_callback(uri, initial_state=initial_state, ctx=ctx, headless=headless)
190-
199+
info = process_browser_callback(
200+
uri, initial_state=initial_state, ctx=ctx, headless=headless
201+
)
191202

192203
if info:
193204
if info.get("email", None):
@@ -198,50 +209,60 @@ def login(
198209
if headless:
199210
console.print()
200211

201-
render_successful_login(ctx.obj.auth, organization=organization)
202-
203212
initialize(ctx, refresh=True)
213+
initialize_event_bus(ctx=ctx)
214+
render_successful_login(ctx.obj.auth, organization=organization)
204215

205216
console.print()
206217
if ctx.obj.auth.org or ctx.obj.auth.email_verified:
207218
console.print(
208-
"[tip]Tip[/tip]: now try [bold]`safety scan`[/bold] in your project’s root " \
209-
"folder to run a project scan or [bold]`safety -–help`[/bold] to learn more.")
219+
"[tip]Tip[/tip]: now try [bold]`safety scan`[/bold] in your project’s root "
220+
"folder to run a project scan or [bold]`safety -–help`[/bold] to learn more."
221+
)
210222
else:
211-
console.print(MSG_FINISH_REGISTRATION_TPL.format(email=ctx.obj.auth.email))
223+
console.print(
224+
MSG_FINISH_REGISTRATION_TPL.format(email=ctx.obj.auth.email)
225+
)
212226
console.print()
213227
console.print(MSG_VERIFICATION_HINT)
214228
else:
215229
click.secho("Safety is now authenticated but your email is missing.")
216230
else:
217231
msg = ":stop_sign: [red]"
218232
if ctx.obj.auth.org:
219-
msg += f"Error logging into {ctx.obj.auth.org.name} organization " \
233+
msg += (
234+
f"Error logging into {ctx.obj.auth.org.name} organization "
220235
f"with auth ID: {ctx.obj.auth.org.id}."
236+
)
221237
else:
222238
msg += "Error logging into Safety."
223239

224-
msg += " Please try again, or use [bold]`safety auth -–help`[/bold] " \
240+
msg += (
241+
" Please try again, or use [bold]`safety auth -–help`[/bold] "
225242
"for more information[/red]"
243+
)
226244

227245
console.print(msg, emoji=True)
228246

247+
229248
@auth_app.command(name=CMD_LOGOUT_NAME, help=CLI_AUTH_LOGOUT_HELP)
249+
@handle_cmd_exception
250+
@notify
230251
def logout(ctx: typer.Context) -> None:
231252
"""
232253
Log out of your current session.
233254
234255
Args:
235256
ctx (typer.Context): The Typer context object.
236257
"""
237-
LOG.info('logout started')
258+
LOG.info("logout started")
238259

239-
id_token = get_token('id_token')
260+
id_token = get_token("id_token")
240261

241262
msg = MSG_NON_AUTHENTICATED
242263

243264
if id_token:
244-
# Clean the session if an ID token is found
265+
# Clean the session if an ID token is found
245266
if clean_session(ctx.obj.auth.client):
246267
msg = MSG_LOGOUT_DONE
247268
else:
@@ -251,13 +272,23 @@ def logout(ctx: typer.Context) -> None:
251272

252273

253274
@auth_app.command(name=CMD_STATUS_NAME, help=CLI_AUTH_STATUS_HELP)
254-
@click.option("--ensure-auth/--no-ensure-auth", default=False,
255-
help="This will keep running the command until an" \
256-
"authentication is made.")
257-
@click.option("--login-timeout", "-w", type=int, default=600,
258-
help="Max time allowed to wait for an authentication.")
259-
def status(ctx: typer.Context, ensure_auth: bool = False,
260-
login_timeout: int = 600) -> None:
275+
@click.option(
276+
"--ensure-auth/--no-ensure-auth",
277+
default=False,
278+
help="This will keep running the command until anauthentication is made.",
279+
)
280+
@click.option(
281+
"--login-timeout",
282+
"-w",
283+
type=int,
284+
default=600,
285+
help="Max time allowed to wait for an authentication.",
286+
)
287+
@handle_cmd_exception
288+
@notify
289+
def status(
290+
ctx: typer.Context, ensure_auth: bool = False, login_timeout: int = 600
291+
) -> None:
261292
"""
262293
Display Safety CLI's current authentication status.
263294
@@ -266,7 +297,7 @@ def status(ctx: typer.Context, ensure_auth: bool = False,
266297
ensure_auth (bool): Whether to keep running until authentication is made.
267298
login_timeout (int): Max time allowed to wait for authentication.
268299
"""
269-
LOG.info('status started')
300+
LOG.info("status started")
270301
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
271302
safety_version = get_version()
272303
console.print(f"[{current_time}]: Safety {safety_version}")
@@ -283,20 +314,28 @@ def status(ctx: typer.Context, ensure_auth: bool = False,
283314
verified = is_email_verified(info)
284315
email_status = " [red](email not verified)[/red]" if not verified else ""
285316

286-
console.print(f'[green]Authenticated as {info["email"]}[/green]{email_status}')
317+
console.print(f"[green]Authenticated as {info['email']}[/green]{email_status}")
287318
elif ensure_auth:
288-
console.print('Safety is not authenticated. Launching default browser to log in')
319+
console.print(
320+
"Safety is not authenticated. Launching default browser to log in"
321+
)
289322
console.print()
290-
uri, initial_state = get_authorization_data(client=ctx.obj.auth.client,
291-
code_verifier=ctx.obj.auth.code_verifier,
292-
organization=ctx.obj.auth.org, ensure_auth=ensure_auth)
323+
uri, initial_state = get_authorization_data(
324+
client=ctx.obj.auth.client,
325+
code_verifier=ctx.obj.auth.code_verifier,
326+
organization=ctx.obj.auth.org,
327+
ensure_auth=ensure_auth,
328+
)
293329

294330
# Process the browser callback to complete the authentication
295-
info = process_browser_callback(uri, initial_state=initial_state,
296-
timeout=login_timeout, ctx=ctx)
331+
info = process_browser_callback(
332+
uri, initial_state=initial_state, timeout=login_timeout, ctx=ctx
333+
)
297334

298335
if not info:
299-
console.print(f'[red]Timeout error ({login_timeout} seconds): not successfully authenticated without the timeout period.[/red]')
336+
console.print(
337+
f"[red]Timeout error ({login_timeout} seconds): not successfully authenticated without the timeout period.[/red]"
338+
)
300339
sys.exit(1)
301340

302341
organization = None
@@ -311,33 +350,37 @@ def status(ctx: typer.Context, ensure_auth: bool = False,
311350

312351

313352
@auth_app.command(name=CMD_REGISTER_NAME)
353+
@handle_cmd_exception
354+
@notify
314355
def register(ctx: typer.Context) -> None:
315356
"""
316357
Create a new user account for the safetycli.com service.
317358
318359
Args:
319360
ctx (typer.Context): The Typer context object.
320361
"""
321-
LOG.info('register started')
362+
LOG.info("register started")
322363

323364
# Check if the user is already authenticated
324365
fail_if_authenticated(ctx, with_msg=MSG_FAIL_REGISTER_AUTHED)
325366

326367
# Get authorization data and generate the registration URL
327-
uri, initial_state = get_authorization_data(client=ctx.obj.auth.client,
328-
code_verifier=ctx.obj.auth.code_verifier,
329-
sign_up=True)
330-
331-
console.print("Redirecting your browser to register for a free account. Once registered, return here to start using Safety.")
368+
uri, initial_state = get_authorization_data(
369+
client=ctx.obj.auth.client,
370+
code_verifier=ctx.obj.auth.code_verifier,
371+
sign_up=True,
372+
)
373+
374+
console.print(
375+
"Redirecting your browser to register for a free account. Once registered, return here to start using Safety."
376+
)
332377
console.print()
333378

334379
# Process the browser callback to complete the registration
335-
info = process_browser_callback(uri,
336-
initial_state=initial_state, ctx=ctx)
380+
info = process_browser_callback(uri, initial_state=initial_state, ctx=ctx)
337381

338382
if info:
339-
console.print(f'[green]Successfully registered {info.get("email")}[/green]')
383+
console.print(f"[green]Successfully registered {info.get('email')}[/green]")
340384
console.print()
341385
else:
342-
console.print('[red]Unable to register in this time, try again.[/red]')
343-
386+
console.print("[red]Unable to register in this time, try again.[/red]")

0 commit comments

Comments
 (0)