finops-scan 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
File without changes
@@ -0,0 +1,217 @@
1
+ """
2
+ Analyzer — derives waste insights from Cost Explorer data.
3
+ No AWS API calls here — pure computation on DailySpend records.
4
+ Separated from fetcher so it can be unit tested without AWS credentials.
5
+ """
6
+ from dataclasses import dataclass
7
+ from decimal import Decimal
8
+ from typing import List, Dict
9
+ from src.finops_scan.fetcher import DailySpend
10
+
11
+
12
+ @dataclass
13
+ class ServiceSummary:
14
+ service: str
15
+ total_30d: Decimal
16
+ daily_average: Decimal
17
+ trend: str # "increasing" | "decreasing" | "stable"
18
+
19
+
20
+ @dataclass
21
+ class WasteOpportunity:
22
+ rank: int
23
+ service: str
24
+ finding: str
25
+ monthly_cost: Decimal
26
+ action: str
27
+
28
+
29
+ class CostAnalyzer:
30
+ """
31
+ Derives ranked waste opportunities from DailySpend records.
32
+
33
+ All methods are pure functions — same input always produces
34
+ same output. No side effects, no API calls.
35
+ Easy to unit test without AWS credentials.
36
+ """
37
+
38
+ # Services that typically indicate waste when spend is high
39
+ # relative to their utility
40
+ IDLE_SIGNALS = {
41
+ "Amazon Elastic Compute Cloud - Compute": "EC2 instances",
42
+ "Amazon Relational Database Service": "RDS instances",
43
+ "Amazon ElastiCache": "ElastiCache clusters",
44
+ "Amazon Redshift": "Redshift clusters",
45
+ "AWS Lambda": "Lambda functions",
46
+ }
47
+
48
+ AI_SERVICES = {
49
+ "Amazon Bedrock",
50
+ "Amazon SageMaker",
51
+ }
52
+
53
+ def summarize_by_service(
54
+ self,
55
+ records: List[DailySpend],
56
+ ) -> List[ServiceSummary]:
57
+ """
58
+ Aggregates DailySpend records into per-service summaries.
59
+ Sorted by total_30d descending — highest spend first.
60
+ """
61
+ totals: Dict[str, Decimal] = {}
62
+ counts: Dict[str, int] = {}
63
+ first7: Dict[str, Decimal] = {}
64
+ last7: Dict[str, Decimal] = {}
65
+
66
+ sorted_records = sorted(records, key=lambda r: r.date)
67
+ all_dates = sorted(set(r.date for r in sorted_records))
68
+ first_7_dates = set(all_dates[:7])
69
+ last_7_dates = set(all_dates[-7:])
70
+
71
+ for r in records:
72
+ totals[r.service] = totals.get(r.service, Decimal("0")) + r.amount
73
+ counts[r.service] = counts.get(r.service, 0) + 1
74
+ if r.date in first_7_dates:
75
+ first7[r.service] = first7.get(r.service,
76
+ Decimal("0")) + r.amount
77
+ if r.date in last_7_dates:
78
+ last7[r.service] = last7.get(r.service,
79
+ Decimal("0")) + r.amount
80
+
81
+ summaries = []
82
+ for service, total in totals.items():
83
+ count = counts[service]
84
+ daily_avg = total / count if count > 0 else Decimal("0")
85
+ f = first7.get(service, Decimal("0"))
86
+ l = last7.get(service, Decimal("0"))
87
+ if l > f * Decimal("1.10"):
88
+ trend = "increasing"
89
+ elif l < f * Decimal("0.90"):
90
+ trend = "decreasing"
91
+ else:
92
+ trend = "stable"
93
+
94
+ summaries.append(ServiceSummary(
95
+ service=service,
96
+ total_30d=total,
97
+ daily_average=daily_avg,
98
+ trend=trend,
99
+ ))
100
+
101
+ return sorted(summaries, key=lambda s: s.total_30d, reverse=True)
102
+
103
+ def identify_waste(
104
+ self,
105
+ summaries: List[ServiceSummary],
106
+ tag_coverage: dict,
107
+ ) -> List[WasteOpportunity]:
108
+ """
109
+ Produces ranked waste opportunities from service summaries.
110
+ Each opportunity has a specific finding and action.
111
+ """
112
+ opportunities: List[WasteOpportunity] = []
113
+ rank = 1
114
+
115
+ # Opportunity 1: increasing spend on compute services
116
+ for s in summaries:
117
+ if s.trend == "increasing" and s.service in self.IDLE_SIGNALS:
118
+ name = self.IDLE_SIGNALS[s.service]
119
+ opportunities.append(WasteOpportunity(
120
+ rank=rank,
121
+ service=s.service,
122
+ finding=(
123
+ f"{name} spend increased 10%+ over the last 7 days. "
124
+ f"30-day total: ${s.total_30d:,.2f}"
125
+ ),
126
+ monthly_cost=s.total_30d,
127
+ action=(
128
+ f"Review {name} utilization. "
129
+ f"Rightsizing or stopping idle instances "
130
+ f"could reduce this cost by 20-40%."
131
+ ),
132
+ ))
133
+ rank += 1
134
+
135
+ # Opportunity 2: high spend on data transfer
136
+ for s in summaries:
137
+ if "Data Transfer" in s.service and s.total_30d > Decimal("100"):
138
+ opportunities.append(WasteOpportunity(
139
+ rank=rank,
140
+ service=s.service,
141
+ finding=(
142
+ f"Data transfer cost ${s.total_30d:,.2f} in 30 days. "
143
+ f"Often caused by cross-AZ traffic or "
144
+ f"unoptimized S3 access patterns."
145
+ ),
146
+ monthly_cost=s.total_30d,
147
+ action=(
148
+ "Move resources to the same AZ. "
149
+ "Use S3 VPC endpoints to eliminate "
150
+ "data transfer charges."
151
+ ),
152
+ ))
153
+ rank += 1
154
+
155
+ # Opportunity 3: low tag coverage
156
+ if tag_coverage.get("untagged_pct", 100) > 20:
157
+ untagged = tag_coverage.get("untagged_spend", Decimal("0"))
158
+ opportunities.append(WasteOpportunity(
159
+ rank=rank,
160
+ service="All services",
161
+ finding=(
162
+ f"{tag_coverage['untagged_pct']}% of spend "
163
+ f"(${untagged:,.2f}) has no team tag. "
164
+ f"You cannot allocate what you cannot attribute."
165
+ ),
166
+ monthly_cost=untagged,
167
+ action=(
168
+ "Apply team tags to all resources. "
169
+ "fynC can automate tag suggestions "
170
+ "based on resource naming patterns."
171
+ ),
172
+ ))
173
+ rank += 1
174
+
175
+ # Opportunity 4: AI services with no visibility
176
+ ai_spend = sum(
177
+ s.total_30d for s in summaries
178
+ if any(ai in s.service for ai in self.AI_SERVICES)
179
+ )
180
+ if ai_spend > Decimal("0"):
181
+ opportunities.append(WasteOpportunity(
182
+ rank=rank,
183
+ service="AI Services",
184
+ finding=(
185
+ f"${ai_spend:,.2f} spent on AI services in 30 days "
186
+ f"with no model-level breakdown. "
187
+ f"You cannot optimize what you cannot see."
188
+ ),
189
+ monthly_cost=ai_spend,
190
+ action=(
191
+ "Track AI spend at the model level with fynC. "
192
+ "See which team is driving cost and "
193
+ "which model gives the best cost/quality ratio."
194
+ ),
195
+ ))
196
+ rank += 1
197
+
198
+ return opportunities[:10] # top 10 only
199
+
200
+ def total_spend(self, records: List[DailySpend]) -> Decimal:
201
+ """Total spend across all services for the period."""
202
+ return sum((r.amount for r in records), Decimal("0"))
203
+
204
+ def estimated_savings(
205
+ self,
206
+ opportunities: List[WasteOpportunity],
207
+ ) -> Decimal:
208
+ """
209
+ Conservative savings estimate: 20% of waste opportunity costs.
210
+ Real savings depend on which actions are taken.
211
+ """
212
+ total = sum(
213
+ (o.monthly_cost for o in opportunities
214
+ if o.service != "All services"),
215
+ Decimal("0"),
216
+ )
217
+ return total * Decimal("0.20")
finops_scan/cli.py ADDED
@@ -0,0 +1,118 @@
1
+ """
2
+ CLI entry point for finops-scan.
3
+ Uses Click for argument parsing and command structure.
4
+ Orchestrates: fetcher → analyzer → reporter.
5
+ """
6
+ import sys
7
+ from decimal import Decimal
8
+
9
+ import click
10
+ from rich.console import Console
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.command()
16
+ @click.option(
17
+ "--access-key",
18
+ envvar="AWS_ACCESS_KEY_ID",
19
+ required=True,
20
+ help="AWS Access Key ID (or set AWS_ACCESS_KEY_ID env var)",
21
+ )
22
+ @click.option(
23
+ "--secret-key",
24
+ envvar="AWS_SECRET_ACCESS_KEY",
25
+ required=True,
26
+ help="AWS Secret Access Key (or set AWS_SECRET_ACCESS_KEY env var)",
27
+ )
28
+ @click.option(
29
+ "--region",
30
+ default="us-east-1",
31
+ show_default=True,
32
+ help="AWS region",
33
+ )
34
+ @click.option(
35
+ "--account-id",
36
+ default="my-account",
37
+ help="AWS account ID or alias (display only)",
38
+ )
39
+ def main(
40
+ access_key: str,
41
+ secret_key: str,
42
+ region: str,
43
+ account_id: str,
44
+ ) -> None:
45
+ """
46
+ finops-scan — scan your AWS account for cloud waste in 60 seconds.
47
+
48
+ Reads cost data via AWS Cost Explorer API (read-only).
49
+ No data is stored. No resources are modified.
50
+
51
+ Required IAM permission: ce:GetCostAndUsage
52
+
53
+ Example:
54
+
55
+ finops-scan --access-key AKIA... --secret-key ... --region us-east-1
56
+
57
+ Or use environment variables:
58
+
59
+ export AWS_ACCESS_KEY_ID=AKIA...
60
+
61
+ export AWS_SECRET_ACCESS_KEY=...
62
+
63
+ finops-scan
64
+ """
65
+ from src.finops_scan.fetcher import CostExplorerFetcher
66
+ from src.finops_scan.analyzer import CostAnalyzer
67
+ from src.finops_scan.reporter import WasteReporter
68
+
69
+ reporter = WasteReporter()
70
+ reporter.print_header(account_id)
71
+
72
+ # Step 1 — Fetch
73
+ console.print("[dim]Fetching cost data from AWS Cost Explorer...[/dim]")
74
+ try:
75
+ fetcher = CostExplorerFetcher(
76
+ aws_access_key=access_key,
77
+ aws_secret_key=secret_key,
78
+ region=region,
79
+ )
80
+ records = fetcher.fetch_last_30_days()
81
+ tag_coverage = fetcher.fetch_tag_coverage()
82
+
83
+ except Exception as e:
84
+ console.print(f"[red]✗ Failed to connect to AWS:[/red] {e}")
85
+ console.print(
86
+ "[dim]Check your credentials and ensure "
87
+ "ce:GetCostAndUsage permission is attached.[/dim]"
88
+ )
89
+ sys.exit(1)
90
+
91
+ if not records:
92
+ console.print(
93
+ "[yellow]No cost data found for the last 30 days.[/yellow]"
94
+ )
95
+ sys.exit(0)
96
+
97
+ console.print(
98
+ f"[dim]✓ Fetched {len(records)} cost records.[/dim]"
99
+ )
100
+
101
+ # Step 2 — Analyze
102
+ console.print("[dim]Analyzing waste patterns...[/dim]")
103
+ analyzer = CostAnalyzer()
104
+ summaries = analyzer.summarize_by_service(records)
105
+ opportunities = analyzer.identify_waste(summaries, tag_coverage)
106
+ total = analyzer.total_spend(records)
107
+ savings = analyzer.estimated_savings(opportunities)
108
+ console.print("[dim]✓ Analysis complete.[/dim]\n")
109
+
110
+ # Step 3 — Report
111
+ reporter.print_summary(total, savings, tag_coverage)
112
+ reporter.print_top_services(summaries)
113
+ reporter.print_waste_opportunities(opportunities)
114
+ reporter.print_cta()
115
+
116
+
117
+ if __name__ == "__main__":
118
+ main()
finops_scan/fetcher.py ADDED
@@ -0,0 +1,142 @@
1
+ """
2
+ AWS Cost Explorer fetcher.
3
+ Pulls cost and usage data using the Cost Explorer API.
4
+ Works on any AWS account — no CUR or S3 setup required.
5
+ Just needs Access Key + Secret Key with ce:GetCostAndUsage permission.
6
+ """
7
+ from dataclasses import dataclass
8
+ from datetime import date, timedelta
9
+ from decimal import Decimal
10
+ from typing import List, Optional
11
+
12
+ import boto3
13
+
14
+
15
+ @dataclass
16
+ class DailySpend:
17
+ date: str
18
+ service: str
19
+ amount: Decimal
20
+ currency: str = "USD"
21
+
22
+
23
+ @dataclass
24
+ class ResourceWaste:
25
+ service: str
26
+ resource_id: str
27
+ monthly_cost: Decimal
28
+ reason: str
29
+ savings: Decimal
30
+
31
+
32
+ class CostExplorerFetcher:
33
+ """
34
+ Fetches cost data from AWS Cost Explorer API.
35
+
36
+ Required IAM permissions:
37
+ ce:GetCostAndUsage
38
+ ce:GetCostAndUsageWithResources (optional — for resource-level)
39
+
40
+ These are read-only permissions. fynC and finops-scan
41
+ never write to or modify any AWS resources.
42
+ """
43
+
44
+ def __init__(
45
+ self,
46
+ aws_access_key: str,
47
+ aws_secret_key: str,
48
+ region: str = "us-east-1",
49
+ ) -> None:
50
+ self._client = boto3.client(
51
+ "ce",
52
+ aws_access_key_id=aws_access_key,
53
+ aws_secret_access_key=aws_secret_key,
54
+ region_name=region,
55
+ )
56
+
57
+ def fetch_last_30_days(self) -> List[DailySpend]:
58
+ """
59
+ Fetches daily spend by service for the last 30 days.
60
+ Returns a flat list of DailySpend records.
61
+ """
62
+ end = date.today()
63
+ start = end - timedelta(days=30)
64
+
65
+ response = self._client.get_cost_and_usage(
66
+ TimePeriod={
67
+ "Start": start.isoformat(),
68
+ "End": end.isoformat(),
69
+ },
70
+ Granularity="DAILY",
71
+ Metrics=["BlendedCost"],
72
+ GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
73
+ )
74
+
75
+ records: List[DailySpend] = []
76
+
77
+ for result in response.get("ResultsByTime", []):
78
+ period = result["TimePeriod"]["Start"]
79
+ for group in result.get("Groups", []):
80
+ service = group["Keys"][0]
81
+ amount = Decimal(
82
+ group["Metrics"]["BlendedCost"]["Amount"]
83
+ )
84
+ if amount > Decimal("0.001"): # skip negligible costs
85
+ records.append(DailySpend(
86
+ date=period,
87
+ service=service,
88
+ amount=amount,
89
+ ))
90
+
91
+ return records
92
+
93
+ def fetch_tag_coverage(self) -> dict:
94
+ """
95
+ Fetches tag coverage — what fraction of spend has team tags.
96
+ Returns dict with tagged_pct, untagged_pct, untagged_spend.
97
+ """
98
+ end = date.today()
99
+ start = end - timedelta(days=30)
100
+
101
+ tagged = self._client.get_cost_and_usage(
102
+ TimePeriod={"Start": start.isoformat(), "End": end.isoformat()},
103
+ Granularity="MONTHLY",
104
+ Metrics=["BlendedCost"],
105
+ Filter={
106
+ "Tags": {
107
+ "Key": "team",
108
+ "Values": ["*"],
109
+ "MatchOptions": ["EXISTS"],
110
+ }
111
+ },
112
+ )
113
+
114
+ total = self._client.get_cost_and_usage(
115
+ TimePeriod={"Start": start.isoformat(), "End": end.isoformat()},
116
+ Granularity="MONTHLY",
117
+ Metrics=["BlendedCost"],
118
+ )
119
+
120
+ def extract(resp) -> Decimal:
121
+ results = resp.get("ResultsByTime", [])
122
+ if not results:
123
+ return Decimal("0")
124
+ return Decimal(
125
+ results[0]["Total"]["BlendedCost"]["Amount"]
126
+ )
127
+
128
+ tagged_spend = extract(tagged)
129
+ total_spend = extract(total)
130
+
131
+ if total_spend == Decimal("0"):
132
+ return {"tagged_pct": 0.0, "untagged_pct": 100.0,
133
+ "untagged_spend": Decimal("0")}
134
+
135
+ tagged_pct = float(tagged_spend / total_spend * 100)
136
+ untagged_pct = 100.0 - tagged_pct
137
+
138
+ return {
139
+ "tagged_pct": round(tagged_pct, 1),
140
+ "untagged_pct": round(untagged_pct, 1),
141
+ "untagged_spend": total_spend - tagged_spend,
142
+ }
@@ -0,0 +1,162 @@
1
+ """
2
+ Reporter — formats and prints the waste report to the terminal.
3
+ Uses Rich for colored, structured output.
4
+ Separated from analyzer: formatting concerns are independent
5
+ of business logic. Easy to add JSON or CSV output later.
6
+ """
7
+ from decimal import Decimal
8
+ from typing import List
9
+
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ from rich.panel import Panel
13
+ from rich.text import Text
14
+ from rich import box
15
+
16
+ from src.finops_scan.analyzer import ServiceSummary, WasteOpportunity
17
+
18
+ console = Console()
19
+
20
+
21
+ class WasteReporter:
22
+ """
23
+ Prints the finops-scan waste report to the terminal.
24
+ All output goes through Rich for consistent formatting.
25
+ """
26
+
27
+ TREND_ICONS = {
28
+ "increasing": "[red]↑[/red]",
29
+ "decreasing": "[green]↓[/green]",
30
+ "stable": "[yellow]→[/yellow]",
31
+ }
32
+
33
+ def print_header(self, account_id: str) -> None:
34
+ """Prints the fynC branded header."""
35
+ console.print()
36
+ console.print(Panel(
37
+ Text.from_markup(
38
+ "[bold #E8460A]finops-scan[/bold #E8460A] "
39
+ "[dim]by fynC[/dim]\n"
40
+ "[dim]Cloud waste scanner · "
41
+ "Read-only · No data stored[/dim]"
42
+ ),
43
+ border_style="#E8460A",
44
+ padding=(0, 2),
45
+ ))
46
+ console.print(
47
+ f"[dim]Account:[/dim] [bold]{account_id}[/bold] "
48
+ f"[dim]Period:[/dim] [bold]Last 30 days[/bold]"
49
+ )
50
+ console.print()
51
+
52
+ def print_summary(
53
+ self,
54
+ total_spend: Decimal,
55
+ estimated_savings: Decimal,
56
+ tag_coverage: dict,
57
+ ) -> None:
58
+ """Prints the 3-number KPI summary at the top."""
59
+ table = Table(box=box.SIMPLE, show_header=False, padding=(0, 3))
60
+ table.add_column(style="dim")
61
+ table.add_column(style="bold")
62
+
63
+ table.add_row(
64
+ "Total 30-day spend",
65
+ f"[bold white]${total_spend:,.2f}[/bold white]",
66
+ )
67
+ table.add_row(
68
+ "Estimated savings available",
69
+ f"[bold green]${estimated_savings:,.2f}[/bold green]",
70
+ )
71
+ table.add_row(
72
+ "Tag coverage",
73
+ f"[bold {'green' if tag_coverage['tagged_pct'] >= 80 else 'red'}]"
74
+ f"{tag_coverage['tagged_pct']}%[/bold]",
75
+ )
76
+
77
+ console.print(Panel(table, title="[bold]Summary[/bold]",
78
+ border_style="dim"))
79
+
80
+ def print_top_services(
81
+ self,
82
+ summaries: List[ServiceSummary],
83
+ top_n: int = 8,
84
+ ) -> None:
85
+ """Prints the top N services by spend."""
86
+ table = Table(
87
+ title="Top Services by Spend",
88
+ box=box.SIMPLE_HEAD,
89
+ show_lines=False,
90
+ title_style="bold",
91
+ )
92
+ table.add_column("#", style="dim", width=3)
93
+ table.add_column("Service", style="white", min_width=35)
94
+ table.add_column("30-day", style="bold", justify="right")
95
+ table.add_column("Daily avg", justify="right")
96
+ table.add_column("Trend", justify="center")
97
+
98
+ for i, s in enumerate(summaries[:top_n], 1):
99
+ # Shorten long AWS service names
100
+ name = s.service.replace(
101
+ "Amazon Elastic Compute Cloud - Compute", "EC2 Compute"
102
+ ).replace(
103
+ "Amazon Relational Database Service", "RDS"
104
+ ).replace(
105
+ "Amazon Simple Storage Service", "S3"
106
+ )
107
+ table.add_row(
108
+ str(i),
109
+ name,
110
+ f"${s.total_30d:,.2f}",
111
+ f"${s.daily_average:,.2f}",
112
+ self.TREND_ICONS.get(s.trend, "→"),
113
+ )
114
+
115
+ console.print(table)
116
+
117
+ def print_waste_opportunities(
118
+ self,
119
+ opportunities: List[WasteOpportunity],
120
+ ) -> None:
121
+ """Prints ranked waste opportunities with actions."""
122
+ console.print()
123
+ console.print("[bold]Waste Opportunities[/bold]")
124
+ console.print("[dim]Ranked by estimated impact[/dim]")
125
+ console.print()
126
+
127
+ if not opportunities:
128
+ console.print(
129
+ "[green]✓ No significant waste detected.[/green] "
130
+ "Your account looks well optimized."
131
+ )
132
+ return
133
+
134
+ for opp in opportunities:
135
+ console.print(
136
+ f"[bold #E8460A]#{opp.rank}[/bold #E8460A] "
137
+ f"[bold]{opp.service}[/bold]"
138
+ )
139
+ console.print(
140
+ f" [dim]Finding:[/dim] {opp.finding}"
141
+ )
142
+ console.print(
143
+ f" [dim]Action:[/dim] [green]{opp.action}[/green]"
144
+ )
145
+ console.print()
146
+
147
+ def print_cta(self) -> None:
148
+ """Prints the call to action at the end."""
149
+ console.print(Panel(
150
+ Text.from_markup(
151
+ "[bold]Want this monitored automatically?[/bold]\n\n"
152
+ "fynC tracks your cloud and AI spend continuously,\n"
153
+ "alerts you when costs spike, and attributes every\n"
154
+ "dollar to a team — without any manual work.\n\n"
155
+ "[bold #E8460A]→ fync.io/early-access[/bold #E8460A] "
156
+ "[dim](or reply to the Show HN post)[/dim]"
157
+ ),
158
+ border_style="#E8460A",
159
+ title="[bold #E8460A]fynC[/bold #E8460A]",
160
+ padding=(1, 3),
161
+ ))
162
+ console.print()
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: finops-scan
3
+ Version: 0.1.0
4
+ Summary: Scan your AWS account for cloud waste in 60 seconds
5
+ Project-URL: Homepage, https://fync.io
6
+ Project-URL: Repository, https://github.com/yourusername/finops-scan
7
+ Project-URL: Bug Tracker, https://github.com/yourusername/finops-scan/issues
8
+ License: MIT
9
+ Keywords: aws,cloud,cost,finops,optimization
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: boto3>=1.35.0
20
+ Requires-Dist: click>=8.1.0
21
+ Requires-Dist: rich>=13.0.0
22
+ Description-Content-Type: text/markdown
23
+
24
+ # finops-scan
25
+
26
+ **Scan your AWS account for cloud waste in 60 seconds.**
27
+
28
+ Free, open source, read-only. No data stored. No resources modified.
29
+
30
+ ```bash
31
+ pip install finops-scan
32
+ finops-scan --access-key AKIA... --secret-key ...
33
+ ```
34
+
35
+ ---
36
+
37
+ ## What it does
38
+
39
+ finops-scan connects to your AWS Cost Explorer API (read-only) and produces a ranked waste report in your terminal:
40
+
41
+ - **Top services by spend** — with 30-day trend (increasing/decreasing/stable)
42
+ - **Waste opportunities** — ranked by estimated impact, with specific actions
43
+ - **Tag coverage score** — what % of your spend has team attribution
44
+ - **Estimated savings** — conservative 20% estimate of actionable waste
45
+
46
+ ## Example output
47
+ ╭─────────────────────────────────────────────╮
48
+ │ finops-scan by fynC │
49
+ │ Cloud waste scanner · Read-only │
50
+ ╰─────────────────────────────────────────────╯
51
+ Account: my-aws-account Period: Last 30 days
52
+ Total 30-day spend $18,420.55
53
+ Estimated savings $3,684.11
54
+ Tag coverage 42%
55
+ Top Services by Spend
56
+ Service 30-day Daily avg Trend
57
+ 1 EC2 Compute $9,240.00 $308.00 ↑
58
+ 2 RDS $4,100.00 $136.67 →
59
+ 3 S3 $1,820.00 $60.67 ↓
60
+ Waste Opportunities
61
+ #1 EC2 Compute
62
+ Finding: EC2 Compute spend increased 10%+ over the last 7 days.
63
+ 30-day total: $9,240.00
64
+ Action: Review EC2 utilization. Rightsizing or stopping idle
65
+ instances could reduce this cost by 20-40%.
66
+ #2 All services
67
+ Finding: 58% of spend ($10,683.92) has no team tag.
68
+ You cannot allocate what you cannot attribute.
69
+ Action: Apply team tags to all resources.
70
+ ╭─────────────────────────────────────────────╮
71
+ │ Want this monitored automatically? │
72
+ │ │
73
+ │ fynC tracks your cloud and AI spend │
74
+ │ continuously, alerts you when costs spike, │
75
+ │ and attributes every dollar to a team. │
76
+ │ │
77
+ │ → fync.io/early-access │
78
+ ╰─────────────────────────────────────────────╯
79
+
80
+ ## Required IAM permission
81
+
82
+ finops-scan only needs one permission:
83
+
84
+ ```json
85
+ {
86
+ "Version": "2012-10-17",
87
+ "Statement": [
88
+ {
89
+ "Effect": "Allow",
90
+ "Action": [
91
+ "ce:GetCostAndUsage"
92
+ ],
93
+ "Resource": "*"
94
+ }
95
+ ]
96
+ }
97
+ ```
98
+
99
+ Create a read-only IAM user with this policy and pass the credentials to finops-scan.
100
+
101
+ ## Installation
102
+
103
+ ```bash
104
+ pip install finops-scan
105
+ ```
106
+
107
+ ## Usage
108
+
109
+ ```bash
110
+ # Using flags
111
+ finops-scan \
112
+ --access-key YOUR_ACCESS_KEY \
113
+ --secret-key YOUR_SECRET_KEY \
114
+ --region us-east-1 \
115
+ --account-id my-company
116
+
117
+ # Using environment variables (recommended)
118
+ export AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
119
+ export AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
120
+ finops-scan
121
+ ```
122
+
123
+ ## What it does NOT do
124
+
125
+ - Does not store any data
126
+ - Does not modify any AWS resources
127
+ - Does not require CUR or S3 setup
128
+ - Does not need root AWS credentials
129
+ - Does not send data anywhere except your terminal
130
+
131
+ ## Built by fynC
132
+
133
+ finops-scan is the free CLI companion to
134
+ [fynC](https://fync.io) — a FinOps platform for cloud
135
+ and AI spend visibility.
136
+
137
+ fynC monitors your spend continuously, alerts you on anomalies,
138
+ attributes costs to teams automatically, and tracks AI/LLM spend
139
+ at the model level alongside cloud infrastructure costs.
140
+
141
+ [Join the early access list →](https://fync.io/early-access)
142
+
143
+ ## License
144
+
145
+ MIT
@@ -0,0 +1,9 @@
1
+ finops_scan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ finops_scan/analyzer.py,sha256=_QuPtDOfIRp7H4qPAUzlDYBdhhioW3szWavXR-FPL1g,8127
3
+ finops_scan/cli.py,sha256=3BkI17MBg9mWO78H-wCVO_CWkRKxSgj31sLMRKE3IBc,3300
4
+ finops_scan/fetcher.py,sha256=hMvA5jf96VA5zS9aBGpzj5SlEKr3tzJGzSdUNDmZrxo,4371
5
+ finops_scan/reporter.py,sha256=orY4NiM9TXIqPzg7WEQJgmkd6reI_yo_8ZFebHSGU8I,5647
6
+ finops_scan-0.1.0.dist-info/METADATA,sha256=D1U0cUyLjodM0TpEDLfQvHDtgDhsV9kAyYxb0-6EaeI,4737
7
+ finops_scan-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
8
+ finops_scan-0.1.0.dist-info/entry_points.txt,sha256=UXcJDrrUbxpR0Sj0O2BvQ785_CxkLx48mPCUSpx5bBs,53
9
+ finops_scan-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ finops-scan = finops_scan.cli:main