Skip to content

Commit d65e438

Browse files
committed
Cleanup around region arg
1 parent f012e24 commit d65e438

2 files changed

Lines changed: 178 additions & 58 deletions

File tree

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ It’s a **hygiene layer** built for teams who value safety over automation.
3030

3131
---
3232

33+
### Built For Production Use
34+
35+
**CleanCloud is designed for:**
36+
- ✅ SOC2/ISO27001 compliant environments (read-only, no credentials stored)
37+
- ✅ Multi-region AWS accounts (scans 20+ regions in parallel)
38+
- ✅ Enterprise Azure subscriptions (supports Workload Identity Federation)
39+
- ✅ CI/CD pipelines (exit codes, JSON output, GitHub Actions ready)
40+
41+
**Security-first:**
42+
- 🔒 No `Delete*` or `Modify*` permissions required
43+
- 🔐 OIDC support (no long-lived credentials)
44+
- 📝 Audit-friendly logging
45+
3346
## Quick Start
3447

3548
### Installation

cleancloud/cli.py

Lines changed: 165 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -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
# ========================
338361
def _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+
379477
def _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

Comments
 (0)