@@ -83,7 +83,9 @@ def cli():
8383 "--region" , default = None , help = "Specific region to scan (AWS region or Azure location)"
8484)
8585@click .option (
86- "--all-regions" , is_flag = True , help = "Scan all enabled AWS regions (slower but comprehensive)"
86+ "--all-regions" ,
87+ is_flag = True ,
88+ help = "Scan all regions with resources (auto-detects active regions)" ,
8789)
8890@click .option ("--profile" , default = None , help = "AWS CLI profile name" )
8991@click .option (
@@ -132,17 +134,54 @@ def scan(
132134 """
133135 Scan cloud infrastructure for orphaned and untagged resources.
134136
137+ AWS: Must specify EITHER --region OR --all-regions
138+ Azure: No region required (scans all subscriptions, optionally filter by location)
139+
135140 Examples:
136- cleancloud scan --provider aws # Auto-detect active regions
137- cleancloud scan --provider aws --region us-east-1 # Specific region
138- cleancloud scan --provider aws --all-regions # All enabled regions
139- cleancloud scan --provider azure # All subscriptions
141+ # AWS - specific region
142+ cleancloud scan --provider aws --region us-east-1
143+
144+ # AWS - all active regions
145+ cleancloud scan --provider aws --all-regions
146+
147+ # Azure - all subscriptions
148+ cleancloud scan --provider azure
149+
150+ # Azure - filter by location
151+ cleancloud scan --provider azure --region eastus
140152 """
141153 click .echo ("🔍 Starting CleanCloud scan" )
142154 click .echo (f"Provider: { provider } " )
143155 click .echo ()
144156
145157 try :
158+ # ========================
159+ # Validate region arguments (AWS only)
160+ # ========================
161+ if provider == "aws" :
162+ # AWS requires explicit region choice
163+ if not region and not all_regions :
164+ click .echo ("❌ Error: Must specify either --region or --all-regions for AWS" )
165+ click .echo ()
166+ click .echo ("Examples:" )
167+ click .echo (" cleancloud scan --provider aws --region us-east-1" )
168+ click .echo (" cleancloud scan --provider aws --all-regions" )
169+ click .echo ()
170+ click .echo ("💡 Tip: Use --all-regions to automatically detect and scan" )
171+ click .echo (" regions with resources (volumes, snapshots, logs)" )
172+ sys .exit (EXIT_ERROR )
173+
174+ if region and all_regions :
175+ click .echo ("❌ Error: Cannot specify both --region and --all-regions" )
176+ click .echo ()
177+ click .echo ("Choose one:" )
178+ click .echo (" --region us-east-1 # Scan specific region" )
179+ click .echo (" --all-regions # Scan all active regions" )
180+ sys .exit (EXIT_ERROR )
181+
182+ # Note: Azure doesn't require region validation
183+ # Azure scans all subscriptions by default, region is optional filter
184+
146185 # ------------------------
147186 # Load config (safe)
148187 # ------------------------
@@ -164,36 +203,25 @@ def scan(
164203 if region :
165204 # Explicit region specified
166205 regions_to_scan = [region ]
167- click .echo (f"🎯 Scanning region: { region } (explicit)" )
168-
169- elif all_regions :
170- # All enabled regions
171- regions_to_scan = _get_all_aws_regions (base_session )
172- click .echo (f"🌍 Scanning all { len (regions_to_scan )} enabled regions" )
173- if len (regions_to_scan ) > 5 :
174- click .echo (f" First 5: { ', ' .join (regions_to_scan [:5 ])} " )
175- click .echo (f" ... and { len (regions_to_scan ) - 5 } more" )
176- else :
177- click .echo (f" Regions: { ', ' .join (regions_to_scan )} " )
206+ click .echo (f"🎯 Scanning region: { region } " )
207+ region_selection_mode = "explicit"
178208
179209 else :
180- # Auto-detect active regions (DEFAULT)
181- click .echo ("🔍 Auto-detecting active AWS regions ..." )
210+ # --all-regions: Auto-detect active regions
211+ click .echo ("🔍 Auto-detecting regions with resources ..." )
182212 regions_to_scan = _get_active_aws_regions (base_session )
183213
184214 if regions_to_scan :
185- click .echo (
186- f"✓ Found { len (regions_to_scan )} active regions: { ', ' .join (regions_to_scan )} "
187- )
188- click .echo (
189- " (Regions with EC2 resources - use --all-regions for comprehensive scan)"
190- )
215+ click .echo (f"✓ Found { len (regions_to_scan )} active regions:" )
216+ click .echo (f" { ', ' .join (regions_to_scan )} " )
217+ click .echo (" (Regions with EBS volumes, snapshots, or logs)" )
191218 else :
192- # Fallback if no active regions detected
193- click .echo ("⚠️ No active regions detected, falling back to us-east-1" )
194- click .echo (" Use --region or --all-regions to scan specific regions" )
219+ click .echo ("⚠️ No active regions detected" )
220+ click .echo (" Falling back to us-east-1" )
195221 regions_to_scan = ["us-east-1" ]
196222
223+ region_selection_mode = "all-regions"
224+
197225 click .echo ()
198226
199227 # Scan each region
@@ -203,11 +231,6 @@ def scan(
203231
204232 regions_scanned = regions_to_scan
205233
206- # Add metadata about how regions were selected
207- region_selection_mode = (
208- "explicit" if region else ("all" if all_regions else "auto-detected" )
209- )
210-
211234 # ========================
212235 # Azure scanning
213236 # ========================
@@ -333,14 +356,18 @@ def scan(
333356
334357
335358# ========================
336- # Helper: Get active AWS regions
359+ # Helper: Get active AWS regions (comprehensive check)
337360# ========================
338361def _get_active_aws_regions (session ) -> List [str ]:
339362 """
340- Auto-detect AWS regions that have EC2 resources.
363+ Auto-detect AWS regions that have resources CleanCloud scans.
364+
365+ Only called when user specifies --all-regions flag.
341366
342- This is a quick heuristic - checks if each region has EC2 instances.
343- Much faster than scanning all 25+ regions.
367+ Checks multiple resource types:
368+ - EBS volumes (unattached volumes rule)
369+ - EBS snapshots (old snapshots rule)
370+ - CloudWatch Logs (infinite retention rule)
344371
345372 Returns:
346373 List of region names with resources
@@ -355,27 +382,98 @@ def _get_active_aws_regions(session) -> List[str]:
355382
356383 enabled_regions = [r ["RegionName" ] for r in response ["Regions" ]]
357384 active_regions = []
385+ errors = []
358386
359- # Quick check: which regions have EC2 instances?
387+ # Check each region for CleanCloud-scanned resources
360388 for region in enabled_regions :
361- try :
362- regional_ec2 = session .client ("ec2" , region_name = region )
363- instances = regional_ec2 .describe_instances (MaxResults = 1 )
389+ has_resources , error = _region_has_cleancloud_resources (session , region )
390+
391+ if has_resources :
392+ active_regions .append (region )
393+ elif error :
394+ # Track errors for reporting
395+ errors .append ((region , error ))
364396
365- if instances ["Reservations" ]:
366- active_regions .append (region )
367- except Exception :
368- # If we can't check, skip this region
369- pass
397+ # Report any errors found
398+ if errors :
399+ import click
400+
401+ click .echo ()
402+ click .echo (f"⚠️ Could not check { len (errors )} region(s):" )
403+ for region , error in errors [:5 ]: # Show first 5
404+ click .echo (f" • { region } : { error [:80 ]} " )
405+ if len (errors ) > 5 :
406+ click .echo (f" ... and { len (errors ) - 5 } more" )
407+ click .echo ()
370408
371409 return active_regions
372410
373411 except Exception :
374412 # If auto-detection fails, return empty list
375- # Caller will handle fallback
376413 return []
377414
378415
416+ def _region_has_cleancloud_resources (session , region : str ) -> tuple [bool , Optional [str ]]:
417+ """
418+ Check if region has any resources that CleanCloud scans.
419+
420+ Checks all CleanCloud rules:
421+ 1. EBS volumes (unattached volumes rule)
422+ 2. EBS snapshots (old snapshots rule)
423+ 3. CloudWatch Logs (infinite retention rule)
424+
425+ Returns:
426+ Tuple of (has_resources, error_message)
427+ - (True, None) = Has resources
428+ - (False, None) = No resources found (empty region)
429+ - (False, "error message") = Error checking region
430+ """
431+ try :
432+ ec2 = session .client ("ec2" , region_name = region )
433+
434+ # 1. Check EBS volumes
435+ # Note: Use MaxResults=5 - some regions don't accept MaxResults=1
436+ volumes = ec2 .describe_volumes (MaxResults = 5 )
437+ if volumes ["Volumes" ]:
438+ return True , None
439+
440+ # 2. Check EBS snapshots (owned by this account)
441+ # Note: AWS requires MaxResults >= 5 for snapshots
442+ snapshots = ec2 .describe_snapshots (OwnerIds = ["self" ], MaxResults = 5 )
443+ if snapshots ["Snapshots" ]:
444+ return True , None
445+
446+ # 3. Check CloudWatch Logs
447+ logs = session .client ("logs" , region_name = region )
448+ log_groups = logs .describe_log_groups (limit = 1 )
449+ if log_groups ["logGroups" ]:
450+ return True , None
451+
452+ # No resources found - this is OK, just an empty region
453+ return False , None
454+
455+ except Exception as e :
456+ # Error checking region - could be permissions, throttling, etc.
457+ error_msg = str (e )
458+
459+ # Check if it's a permission/auth error
460+ if any (
461+ keyword in error_msg .lower ()
462+ for keyword in [
463+ "unauthorized" ,
464+ "access denied" ,
465+ "forbidden" ,
466+ "credentials" ,
467+ "authentication" ,
468+ "not authorized" ,
469+ ]
470+ ):
471+ return False , f"Permission error: { error_msg } "
472+
473+ # Other errors (throttling, network, etc.)
474+ return False , f"Error: { error_msg } "
475+
476+
379477def _print_summary (summary : dict , region_selection_mode : str = None ):
380478 """Print scan summary with region selection context."""
381479 click .echo ("\n --- Scan Summary ---" )
@@ -392,13 +490,10 @@ def _print_summary(summary: dict, region_selection_mode: str = None):
392490 click .echo (f"Regions scanned: { regions_str } " , nl = False )
393491
394492 # Add context about region selection
395- if region_selection_mode == "auto-detected " :
493+ if region_selection_mode == "all-regions " :
396494 click .echo (" (auto-detected)" )
397- click .echo (" 💡 Tip: Use --all-regions for comprehensive scan" )
398495 elif region_selection_mode == "explicit" :
399496 click .echo (" (explicit)" )
400- elif region_selection_mode == "all" :
401- click .echo (" (all enabled)" )
402497 else :
403498 click .echo ()
404499
@@ -417,21 +512,30 @@ def _print_summary(summary: dict, region_selection_mode: str = None):
417512@cli .command ()
418513@click .option (
419514 "--provider" ,
420- default = None , # Changed from "aws" to None
515+ default = None ,
421516 type = click .Choice (["aws" , "azure" ]),
422517 help = "Cloud provider to validate (omit to check both)" ,
423518)
424- @click .option ("--region" , default = "us-east-1" )
425- @click .option ("--profile" , default = None )
519+ @click .option ("--region" , default = "us-east-1" , help = "AWS region for validation" )
520+ @click .option ("--profile" , default = None , help = "AWS profile name" )
426521@click .option (
427522 "--config" ,
428523 type = click .Path (exists = True ),
429524 help = "Path to cleancloud.yaml" ,
430525)
431- def doctor (provider : str , region : Optional [str ], profile : Optional [str ], config : Optional [str ]):
526+ def doctor (provider : Optional [str ], region : str , profile : Optional [str ], config : Optional [str ]):
527+ """
528+ Validate cloud credentials and permissions.
529+
530+ Examples:
531+ cleancloud doctor # Check both AWS and Azure
532+ cleancloud doctor --provider aws # Check AWS only
533+ cleancloud doctor --provider azure # Check Azure only
534+ """
432535 click .echo ("🩺 Running CleanCloud doctor" )
536+ click .echo ()
433537
434- run_doctor (provider , profile , region )
538+ run_doctor (provider = provider , profile = profile , region = region )
435539
436540 try :
437541 cfg = CleanCloudConfig .empty ()
@@ -441,11 +545,14 @@ def doctor(provider: str, region: Optional[str], profile: Optional[str], config:
441545 cfg = load_config (raw )
442546
443547 if cfg .tag_filtering and cfg .tag_filtering .enabled :
444- click .echo ("⚠️ Tag filtering is enabled — some findings may be intentionally ignored" )
548+ click .echo ()
549+ click .echo ("ℹ️ Tag filtering is enabled — some findings may be intentionally ignored" )
550+ click .echo ()
445551
446552 except Exception as e :
447- click .echo (f"❌ Doctor failed: { e } " )
448- sys .exit (EXIT_ERROR )
553+ # Config validation failure is not fatal for doctor command
554+ click .echo (f"⚠️ Config validation warning: { e } " )
555+ click .echo ()
449556
450557
451558# ========================
0 commit comments