1+ import json
2+ from pathlib import Path
13
24from rich .box import HEAVY_EDGE
35from rich .console import Console
@@ -16,42 +18,38 @@ def prompt_scanning_subscription(available_subs: list[Subscription]) -> Subscrip
1618 print_subscriptions (available_subs )
1719 console .print ("\n [bold]Scanning Subscription[/bold]" )
1820 console .print (
19- "[dim]What subscription do you want to deploy the AWLS scanner resources to?[/dim]" )
20- sub_id = Prompt .ask ("Subscription ID" ,
21- choices = [sub .id for sub in available_subs ],
22- show_choices = False )
21+ "[dim]What subscription do you want to deploy the AWLS scanner resources to?[/dim]"
22+ )
23+ sub_id = Prompt .ask (
24+ "Subscription ID" , choices = [sub .id for sub in available_subs ], show_choices = False
25+ )
2326
2427 # Find the subscription in our list of available subscriptions
2528 for sub in available_subs :
2629 if sub .id == sub_id :
27- console .print (
28- f"[green]Selected scanning subscription: { sub .name } [/green]" )
30+ console .print (f"[green]Selected scanning subscription: { sub .name } [/green]" )
2931 return sub
3032 raise ValueError (f"Subscription { sub_id } not found" )
3133
3234
33- def prompt_monitored_subscriptions (available_subs : list [Subscription ]) -> tuple [list [Subscription ], IntegrationType ]:
35+ def prompt_monitored_subscriptions (
36+ available_subs : list [Subscription ],
37+ ) -> tuple [list [Subscription ], IntegrationType ]:
3438 """Prompt user to choose which subscriptions to monitor"""
3539 console .print ("\n [bold]Monitored Subscriptions[/bold]" )
3640 console .print ("[dim]Choose which subscriptions to be monitored.[/dim]" )
3741
38- scan_scope = Prompt .ask (
39- "Scan scope" ,
40- choices = ["all" , "exclude" , "specify" ],
41- default = "all"
42- )
42+ scan_scope = Prompt .ask ("Scan scope" , choices = ["all" , "exclude" , "specify" ], default = "all" )
4343
4444 if scan_scope == "all" :
4545 console .print ("[dim]All subscriptions will be monitored.[/dim]" )
4646 return (available_subs , IntegrationType .TENANT )
4747
4848 if scan_scope == "exclude" :
49- console .print (
50- "[dim]Select subscriptions to exclude from monitoring.[/dim]" )
49+ console .print ("[dim]Select subscriptions to exclude from monitoring.[/dim]" )
5150 print_subscriptions (available_subs )
5251
53- excluded_ids = Prompt .ask (
54- "Excluded Subscription IDs (comma-separated)" )
52+ excluded_ids = Prompt .ask ("Excluded Subscription IDs (comma-separated)" )
5553 excluded_subs = []
5654
5755 for sub_id in [s .strip () for s in excluded_ids .split ("," )]:
@@ -60,21 +58,18 @@ def prompt_monitored_subscriptions(available_subs: list[Subscription]) -> tuple[
6058 excluded_subs .append (sub )
6159 break
6260 else :
63- console .print (
64- f"[yellow]Warning: Subscription { sub_id } not found[/yellow]" )
61+ console .print (f"[yellow]Warning: Subscription { sub_id } not found[/yellow]" )
6562
6663 monitored_subs = [s for s in available_subs if s not in excluded_subs ]
67- console .print (
68- "\n [bold]The following subscriptions will be excluded:[/bold]" )
64+ console .print ("\n [bold]The following subscriptions will be excluded:[/bold]" )
6965 print_subscriptions (excluded_subs )
7066 return (monitored_subs , IntegrationType .TENANT )
7167
7268 # specify
7369 console .print ("[dim]Select specific subscriptions to monitor.[/dim]" )
7470 print_subscriptions (available_subs )
7571
76- specified_ids = Prompt .ask (
77- "Specified Subscription IDs (comma-separated)" )
72+ specified_ids = Prompt .ask ("Specified Subscription IDs (comma-separated)" )
7873 specified_subs = []
7974
8075 for sub_id in [s .strip () for s in specified_ids .split ("," )]:
@@ -83,11 +78,9 @@ def prompt_monitored_subscriptions(available_subs: list[Subscription]) -> tuple[
8378 specified_subs .append (sub )
8479 break
8580 else :
86- console .print (
87- f"[yellow]Warning: Subscription { sub_id } not found[/yellow]" )
81+ console .print (f"[yellow]Warning: Subscription { sub_id } not found[/yellow]" )
8882
89- console .print (
90- "\n [bold]Only the following subscriptions will be monitored:[/bold]" )
83+ console .print ("\n [bold]Only the following subscriptions will be monitored:[/bold]" )
9184 print_subscriptions (specified_subs )
9285 return (specified_subs , IntegrationType .SUBSCRIPTION )
9386
@@ -96,9 +89,11 @@ def prompt_nat_gateway() -> bool:
9689 """Prompt user about NAT Gateway usage"""
9790 console .print ("\n [bold]Network Configuration[/bold]" )
9891 console .print (
99- "[dim]We recommend deploying AWLS with a NAT Gateway, but you can opt out if you prefer.[/dim]" )
92+ "[dim]We recommend deploying AWLS with a NAT Gateway, but you can opt out if you prefer.[/dim]"
93+ )
10094 console .print (
101- "[dim]For more details on deploying with/without a NAT Gateway, please refer to the Lacework FortiCNAPP docs:\n https://docs.fortinet.com/document/lacework-forticnapp/24.4.0/new-features/13869/integrating-agentless-workload-security-with-azure?preview_token=44849b8a6bf658c3c2b3#:~:text=deploy%20the%20Agentless%20scanning%20integration%20with%20a%20NAT%20gateway.[/dim]" )
95+ "[dim]For more details on deploying with/without a NAT Gateway, please refer to the Lacework FortiCNAPP docs:\n https://docs.fortinet.com/document/lacework-forticnapp/24.4.0/new-features/13869/integrating-agentless-workload-security-with-azure?preview_token=44849b8a6bf658c3c2b3#:~:text=deploy%20the%20Agentless%20scanning%20integration%20with%20a%20NAT%20gateway.[/dim]"
96+ )
10297 return Confirm .ask ("Use NAT Gateway?" , default = True )
10398
10499
@@ -164,8 +159,7 @@ def print_vm_counts(subscriptions: list[Subscription]) -> None:
164159 console .print (table )
165160
166161 if subscriptions_with_no_vms :
167- console .print (
168- "\n [yellow]Warning: The following subscriptions have no VMs:[/yellow]" )
162+ console .print ("\n [yellow]Warning: The following subscriptions have no VMs:[/yellow]" )
169163 print_subscriptions (subscriptions_with_no_vms )
170164
171165
@@ -178,26 +172,21 @@ def prompt_regions(subscriptions: list[Subscription]) -> list[str]:
178172 # Get unique regions across all subscriptions
179173 detected_regions = {region_name for sub in subscriptions for region_name in sub .regions }
180174 console .print ("\n [bold]Deployment Regions[/bold]" )
181- console .print (
182- "[dim]Enter the Azure regions that you'd like to monitor.[/dim]" )
175+ console .print ("[dim]Enter the Azure regions that you'd like to monitor.[/dim]" )
183176 console .print ("[dim]The scanner will be deployed in these regions.[/dim]" )
184177
185- regions_default = "," .join (
186- sorted (detected_regions )) if detected_regions else None
187- regions_input = Prompt .ask (
188- "Azure regions (comma-separated)" ,
189- default = regions_default
190- )
178+ regions_default = "," .join (sorted (detected_regions )) if detected_regions else None
179+ regions_input = Prompt .ask ("Azure regions (comma-separated)" , default = regions_default )
191180 assert isinstance (regions_input , str )
192181 selected_regions = []
193182 for region_input in regions_input .strip ().split ("," ):
194183 if region_input .strip () in detected_regions :
195184 selected_regions .append (region_input .strip ())
196185 else :
197- console .print (
198- f"[yellow]Warning: Region { region_input .strip ()} not found[/yellow]" )
186+ console .print (f"[yellow]Warning: Region { region_input .strip ()} not found[/yellow]" )
199187 console .print (
200- f"[green]The following regions will be monitored:[/green] { ', ' .join (selected_regions )} " )
188+ f"[green]The following regions will be monitored:[/green] { ', ' .join (selected_regions )} "
189+ )
201190 return selected_regions
202191
203192
@@ -208,7 +197,8 @@ def print_quota_checks(quota_checks: QuotaChecks) -> None:
208197
209198 console .print ("\n [bold]Usage Quota Limits[/bold]" )
210199 console .print (
211- f"Here are the configured and required usage quota limits for subscription [bold]{ scanning_subscription .name } [/bold]:\n " )
200+ f"Here are the configured and required usage quota limits for subscription [bold]{ scanning_subscription .name } [/bold]:\n "
201+ )
212202
213203 # Create quota requirements table
214204 table = Table (show_header = True , header_style = "bold" , box = HEAVY_EDGE )
@@ -246,7 +236,7 @@ def print_quota_checks(quota_checks: QuotaChecks) -> None:
246236 str (check .configured_limit ),
247237 str (check .current_usage ),
248238 str (check .required_quota ),
249- status
239+ status ,
250240 )
251241 # Add a blank row between regions (unless it's the last region)
252242 if region_name != region_names [- 1 ]:
@@ -256,13 +246,12 @@ def print_quota_checks(quota_checks: QuotaChecks) -> None:
256246
257247 # Print summary and help message
258248 if quota_checks .all_checks_pass ():
259- console .print (
260- "\n [green]✅ All configured usage quota limits are sufficient![/green]" )
249+ console .print ("\n [green]✅ All configured usage quota limits are sufficient![/green]" )
261250 else :
251+ console .print ("\n [yellow]⚠️ Some configured usage quota limits are not sufficient.[/yellow]" )
262252 console .print (
263- "\n [yellow]⚠️ Some configured usage quota limits are not sufficient.[/yellow]" )
264- console .print (
265- "Please request quota increases at: https://portal.azure.com/#blade/Microsoft_Azure_Capacity/QuotaRequestBlade" )
253+ "Please request quota increases at: https://portal.azure.com/#blade/Microsoft_Azure_Capacity/QuotaRequestBlade"
254+ )
266255
267256
268257def print_auth_checks (auth_checks : AuthChecks ) -> None :
@@ -271,12 +260,11 @@ def print_auth_checks(auth_checks: AuthChecks) -> None:
271260 monitored_subscriptions_auth_checks = auth_checks .monitored_subscriptions
272261
273262 # print scanning subscription auth check
274- console .print ("\n [bold]Scanning Subscription Authorization Checks[/bold]" )
263+ console .print ("\n [bold]Scanning Subscription Permission Checks[/bold]" )
275264 print_auth_check_table ([scanning_subscription_auth_check ])
276265
277266 # print monitored subscriptions auth checks
278- console .print (
279- "\n [bold]Monitored Subscriptions Authorization Checks[/bold]" )
267+ console .print ("\n [bold]Monitored Subscriptions Permission Checks[/bold]" )
280268 print_auth_check_table (monitored_subscriptions_auth_checks )
281269
282270
@@ -304,8 +292,113 @@ def print_auth_check_table(auth_checks: list[AuthCheck]) -> None:
304292 console .print (table )
305293
306294
307-
308295def print_preflight_check (preflight_check : PreflightCheck ) -> None :
309296 """Print the preflight check results"""
310297 print_quota_checks (preflight_check .usage_quota_checks )
311298 print_auth_checks (preflight_check .auth_checks )
299+ # summarize results
300+ console .print ("\n [bold]Preflight Check Summary[/bold]" )
301+ if preflight_check .usage_quota_checks .all_checks_pass ():
302+ console .print ("[green bold]✅ All usage quota limits are sufficient![/green bold]" )
303+ else :
304+ console .print ("\n [yellow bold]Some usage quota limits are not sufficient.[/yellow bold]" )
305+ for region_name , quota_checks in preflight_check .usage_quota_checks .quota_checks .items ():
306+ console .print (f"\t [yellow]{ region_name } [/yellow]" )
307+ for quota_check in quota_checks :
308+ if not quota_check .success :
309+ console .print (
310+ f"\t \t [yellow]- { quota_check .display_name } (Configured: { quota_check .configured_limit } , Current: { quota_check .current_usage } , Required: { quota_check .required_quota } )[/yellow]"
311+ )
312+ # print auth checks summary
313+ if preflight_check .auth_checks .all_checks_pass ():
314+ console .print ("[green bold]✅ All permission checks passed![/green bold]" )
315+ else :
316+ console .print ("\n [yellow bold]Some permission checks did not pass.[/yellow bold]" )
317+ console .print (
318+ "[yellow]The authenticated principal is missing the following permissions on the following subscriptions:[/yellow]"
319+ )
320+ for auth_check in [
321+ preflight_check .auth_checks .scanning_subscription ,
322+ * preflight_check .auth_checks .monitored_subscriptions ,
323+ ]:
324+ if not auth_check .success :
325+ console .print (f"\t [bold]{ auth_check .subscription .name } [/bold]:" )
326+ for missing_permission in auth_check .missing_permissions :
327+ console .print (
328+ f"\t \t [yellow]- { missing_permission .required_permission } [/yellow]"
329+ )
330+
331+
332+ def output_preflight_check_results_file (
333+ preflight_check : PreflightCheck , path_str : str = "./preflight_report.json"
334+ ) -> None :
335+ """Output the preflight check results to a file"""
336+ path = Path (path_str )
337+ results = {
338+ "deployment_config" : {
339+ "integration_level" : preflight_check .deployment_config .integration_type .value ,
340+ "scanning_subscription" : f"/subscriptions/{ preflight_check .deployment_config .scanning_subscription .id } " ,
341+ "monitored_subscriptions" : [
342+ f"/subscriptions/{ sub .id } "
343+ for sub in preflight_check .deployment_config .monitored_subscriptions
344+ ],
345+ "regions" : preflight_check .deployment_config .regions ,
346+ "use_nat_gateway" : preflight_check .deployment_config .use_nat_gateway ,
347+ },
348+ "vm_count" : {
349+ f"/subscriptions/{ sub .id } " : {
350+ region_name : region .vm_count for region_name , region in sub .regions .items ()
351+ }
352+ for sub in preflight_check .deployment_config .monitored_subscriptions
353+ },
354+ "success" : preflight_check .usage_quota_checks .all_checks_pass ()
355+ and preflight_check .auth_checks .all_checks_pass (),
356+ "permissions_check" : {
357+ "success" : preflight_check .auth_checks .all_checks_pass (),
358+ "scanning_subscription" : {
359+ "scope" : f"/subscriptions/{ preflight_check .auth_checks .scanning_subscription .subscription .id } " ,
360+ "success" : preflight_check .auth_checks .scanning_subscription .success ,
361+ "missing_permissions" : [
362+ check .required_permission
363+ for check in preflight_check .auth_checks .scanning_subscription .missing_permissions
364+ ],
365+ },
366+ "monitored_subscriptions" : [
367+ {
368+ "scope" : f"/subscriptions/{ auth_check .subscription .id } " ,
369+ "success" : auth_check .success ,
370+ "missing_permissions" : [
371+ check .required_permission for check in auth_check .missing_permissions
372+ ],
373+ }
374+ for auth_check in preflight_check .auth_checks .monitored_subscriptions
375+ ],
376+ },
377+ "usage_quota_check" : {
378+ "success" : preflight_check .usage_quota_checks .all_checks_pass (),
379+ "subscription" : f"/subscriptions/{ preflight_check .usage_quota_checks .subscription .id } " ,
380+ "quota_checks" : [
381+ {
382+ "region" : region_name ,
383+ "quotas" : [
384+ {
385+ "name" : check .name ,
386+ "display_name" : check .display_name ,
387+ "required_limit" : check .required_quota ,
388+ "current_usage" : check .current_usage ,
389+ "configured_limit" : check .configured_limit ,
390+ "success" : check .success ,
391+ }
392+ for check in checks
393+ ],
394+ "success" : all (check .success for check in checks ),
395+ }
396+ for region_name , checks in preflight_check .usage_quota_checks .quota_checks .items ()
397+ ],
398+ },
399+ }
400+
401+ with open (path , "w" ) as f :
402+ json .dump (results , f , indent = 2 )
403+
404+ console .print (f"\n Preflight check results written to [bold]{ path } [/bold]\n " )
0 commit comments