sofe 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.
sofe/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """SOFE — Stairway Open FinOps Engine. Policies as Code for AWS."""
2
+ __version__ = "0.1.0"
sofe/cli.py ADDED
@@ -0,0 +1,121 @@
1
+ """SOFE CLI — sofe evaluate, sofe validate."""
2
+
3
+ import json
4
+ import click
5
+ from .loader import load_policies, validate_policies
6
+ from .engine import evaluate
7
+ from .models import Finding
8
+
9
+
10
+ @click.group()
11
+ @click.version_option(version="0.1.0")
12
+ def main():
13
+ """SOFE — Stairway Open FinOps Engine. Policies as Code for AWS."""
14
+ pass
15
+
16
+
17
+ @main.command()
18
+ @click.option("--policies", "-p", required=True, help="Path to policies directory or file")
19
+ @click.option("--format", "-f", "fmt", default="table", type=click.Choice(["table", "json", "markdown"]))
20
+ @click.option("--min-severity", default=None, type=click.Choice(["critical", "high", "medium", "low", "info"]))
21
+ @click.option("--fail-on", default=None, type=click.Choice(["critical", "high", "medium", "low"]))
22
+ @click.option("--profile", default=None, help="AWS profile to use")
23
+ @click.option("--dry-run", is_flag=True, help="Show what would be evaluated without calling AWS")
24
+ def evaluate_cmd(policies, fmt, min_severity, fail_on, profile, dry_run):
25
+ """Evaluate policies against live AWS resources."""
26
+ from .collectors import collect_all
27
+
28
+ click.echo(f"šŸ“‹ Loading policies from: {policies}")
29
+ policy_list = load_policies(policies)
30
+ click.echo(f" Found {len(policy_list)} policies")
31
+
32
+ if dry_run:
33
+ click.echo("\nšŸ” DRY RUN — would evaluate:")
34
+ for p in policy_list:
35
+ click.echo(f" • {p.metadata.name} ({p.spec.severity.value}) → {p.spec.scope.resource_types}")
36
+ return
37
+
38
+ click.echo(f"\nā˜ļø Scanning AWS resources (profile: {profile or 'default'})...")
39
+ resources = collect_all(profile=profile, resource_types=_get_required_types(policy_list))
40
+ click.echo(f" Found {len(resources)} resources")
41
+
42
+ click.echo(f"\n⚔ Evaluating {len(policy_list)} policies against {len(resources)} resources...")
43
+ findings = evaluate(policy_list, resources)
44
+
45
+ # Filter by severity
46
+ if min_severity:
47
+ severity_order = ["critical", "high", "medium", "low", "info"]
48
+ min_idx = severity_order.index(min_severity)
49
+ findings = [f for f in findings if severity_order.index(f.severity.value) <= min_idx]
50
+
51
+ # Output
52
+ if fmt == "json":
53
+ click.echo(json.dumps([f.model_dump(mode="json") for f in findings], indent=2, default=str))
54
+ elif fmt == "markdown":
55
+ _output_markdown(findings)
56
+ else:
57
+ _output_table(findings)
58
+
59
+ # Exit code
60
+ if fail_on and findings:
61
+ severity_order = ["critical", "high", "medium", "low"]
62
+ fail_idx = severity_order.index(fail_on)
63
+ blocking = [f for f in findings if severity_order.index(f.severity.value) <= fail_idx]
64
+ if blocking:
65
+ raise SystemExit(1)
66
+
67
+
68
+ @main.command()
69
+ @click.option("--policies", "-p", required=True, help="Path to policies directory or file")
70
+ def validate(policies):
71
+ """Validate policy YAML files without evaluating."""
72
+ results = validate_policies(policies)
73
+ all_valid = True
74
+ for r in results:
75
+ status = "āœ…" if r["valid"] else "āŒ"
76
+ click.echo(f" {status} {r['file']}")
77
+ if r["error"]:
78
+ click.echo(f" {r['error']}")
79
+ all_valid = False
80
+
81
+ click.echo(f"\n{'All valid āœ…' if all_valid else 'Some invalid āŒ'}")
82
+ if not all_valid:
83
+ raise SystemExit(1)
84
+
85
+
86
+ def _get_required_types(policies) -> list[str]:
87
+ types = set()
88
+ for p in policies:
89
+ types.update(p.spec.scope.resource_types)
90
+ return list(types)
91
+
92
+
93
+ def _output_table(findings: list[Finding]):
94
+ if not findings:
95
+ click.echo("\nāœ… No violations found!")
96
+ return
97
+
98
+ click.echo(f"\n{'─'*80}")
99
+ click.echo(f"{'Severity':<10} {'Policy':<25} {'Resource':<20} {'Message':<25}")
100
+ click.echo(f"{'─'*80}")
101
+ icons = {"critical": "šŸ”“", "high": "🟠", "medium": "🟔", "low": "šŸ”µ", "info": "⚪"}
102
+ for f in findings:
103
+ icon = icons.get(f.severity.value, "⚪")
104
+ click.echo(f"{icon} {f.severity.value:<8} {f.policy_name:<25} {f.resource_id:<20} {f.message:<25}")
105
+
106
+ total_savings = sum(f.estimated_savings or 0 for f in findings)
107
+ click.echo(f"{'─'*80}")
108
+ click.echo(f"Summary: {len(findings)} findings | Potential savings: ${total_savings:.2f}/mo")
109
+
110
+
111
+ def _output_markdown(findings: list[Finding]):
112
+ click.echo(f"# SOFE Evaluation Results\n")
113
+ click.echo(f"**Findings:** {len(findings)}\n")
114
+ click.echo("| Severity | Policy | Resource | Message |")
115
+ click.echo("|----------|--------|----------|---------|")
116
+ for f in findings:
117
+ click.echo(f"| {f.severity.value} | {f.policy_name} | {f.resource_id} | {f.message} |")
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+ """AWS resource collectors for SOFE — scan resources + fetch metrics."""
3
+
4
+ import boto3
5
+ from ..models import Resource
6
+
7
+
8
+ def collect_all(profile: str = None, resource_types: list[str] = None, regions: list[str] = None) -> list[Resource]:
9
+ """Collect resources from AWS using the given profile."""
10
+ session = boto3.Session(profile_name=profile) if profile else boto3.Session()
11
+ account_id = session.client('sts').get_caller_identity()['Account']
12
+ target_regions = regions or [session.region_name or 'us-east-1']
13
+
14
+ resources: list[Resource] = []
15
+ types = resource_types or ['aws.ec2', 'aws.s3', 'aws.lambda', 'aws.rds']
16
+
17
+ for region in target_regions:
18
+ if 'aws.ec2' in types:
19
+ resources.extend(_collect_ec2(session, region, account_id))
20
+ if 'aws.s3' in types:
21
+ resources.extend(_collect_s3(session, region, account_id))
22
+ if 'aws.lambda' in types:
23
+ resources.extend(_collect_lambda(session, region, account_id))
24
+ if 'aws.rds' in types:
25
+ resources.extend(_collect_rds(session, region, account_id))
26
+
27
+ # Fetch metrics for collected resources
28
+ _enrich_metrics(session, resources, target_regions[0])
29
+
30
+ # Add tag-based metrics
31
+ for r in resources:
32
+ for key in ['owner', 'env', 'costCenter', 'Environment', 'Name']:
33
+ r.metrics[f'has_tag:{key}'] = 1.0 if key in r.tags else 0.0
34
+
35
+ return resources
36
+
37
+
38
+ def _collect_ec2(session: boto3.Session, region: str, account_id: str) -> list[Resource]:
39
+ try:
40
+ ec2 = session.client('ec2', region_name=region)
41
+ resp = ec2.describe_instances(Filters=[{'Name': 'instance-state-name', 'Values': ['running']}])
42
+ resources = []
43
+ for res in resp.get('Reservations', []):
44
+ for inst in res.get('Instances', []):
45
+ tags = {t['Key']: t['Value'] for t in inst.get('Tags', [])}
46
+ resources.append(Resource(
47
+ resource_id=inst['InstanceId'],
48
+ resource_type='aws.ec2',
49
+ region=region,
50
+ account_id=account_id,
51
+ tags=tags,
52
+ properties={'instance_type': inst.get('InstanceType'), 'launch_time': str(inst.get('LaunchTime', ''))},
53
+ ))
54
+ return resources
55
+ except Exception as e:
56
+ print(f" āš ļø EC2 scan failed in {region}: {e}")
57
+ return []
58
+
59
+
60
+ def _collect_s3(session: boto3.Session, region: str, account_id: str) -> list[Resource]:
61
+ try:
62
+ s3 = session.client('s3', region_name=region)
63
+ buckets = s3.list_buckets().get('Buckets', [])
64
+ return [Resource(
65
+ resource_id=b['Name'],
66
+ resource_type='aws.s3',
67
+ region='global',
68
+ account_id=account_id,
69
+ properties={'creation_date': str(b.get('CreationDate', ''))},
70
+ ) for b in buckets]
71
+ except Exception as e:
72
+ print(f" āš ļø S3 scan failed: {e}")
73
+ return []
74
+
75
+
76
+ def _collect_lambda(session: boto3.Session, region: str, account_id: str) -> list[Resource]:
77
+ try:
78
+ client = session.client('lambda', region_name=region)
79
+ functions = client.list_functions().get('Functions', [])
80
+ return [Resource(
81
+ resource_id=fn['FunctionName'],
82
+ resource_type='aws.lambda',
83
+ region=region,
84
+ account_id=account_id,
85
+ properties={'runtime': fn.get('Runtime'), 'memory': fn.get('MemorySize')},
86
+ ) for fn in functions]
87
+ except Exception as e:
88
+ print(f" āš ļø Lambda scan failed in {region}: {e}")
89
+ return []
90
+
91
+
92
+ def _collect_rds(session: boto3.Session, region: str, account_id: str) -> list[Resource]:
93
+ try:
94
+ client = session.client('rds', region_name=region)
95
+ instances = client.describe_db_instances().get('DBInstances', [])
96
+ return [Resource(
97
+ resource_id=db['DBInstanceIdentifier'],
98
+ resource_type='aws.rds',
99
+ region=region,
100
+ account_id=account_id,
101
+ properties={'engine': db.get('Engine'), 'class': db.get('DBInstanceClass')},
102
+ ) for db in instances]
103
+ except Exception as e:
104
+ print(f" āš ļø RDS scan failed in {region}: {e}")
105
+ return []
106
+
107
+
108
+ def _enrich_metrics(session: boto3.Session, resources: list[Resource], region: str):
109
+ """Fetch CloudWatch metrics for resources (CPU, cost)."""
110
+ from datetime import datetime, timedelta
111
+ try:
112
+ cw = session.client('cloudwatch', region_name=region)
113
+ ce = session.client('ce', region_name='us-east-1')
114
+ now = datetime.utcnow()
115
+ start = now - timedelta(days=30)
116
+
117
+ for r in resources:
118
+ if r.resource_type == 'aws.ec2':
119
+ try:
120
+ resp = cw.get_metric_statistics(
121
+ Namespace='AWS/EC2', MetricName='CPUUtilization',
122
+ Dimensions=[{'Name': 'InstanceId', 'Value': r.resource_id}],
123
+ StartTime=start, EndTime=now, Period=86400 * 30, Statistics=['Average'],
124
+ )
125
+ if resp['Datapoints']:
126
+ r.metrics['avg_cpu_utilization'] = round(resp['Datapoints'][0]['Average'], 2)
127
+ except:
128
+ pass
129
+
130
+ # Running days
131
+ if 'launch_time' in r.properties and r.properties['launch_time']:
132
+ try:
133
+ from dateutil.parser import parse
134
+ launch = parse(r.properties['launch_time'])
135
+ r.metrics['running_days'] = (now - launch.replace(tzinfo=None)).days
136
+ except:
137
+ pass
138
+
139
+ # Monthly cost (simplified: total / resource count as estimate)
140
+ try:
141
+ s = (now.replace(day=1) - timedelta(days=1)).replace(day=1).strftime('%Y-%m-%d')
142
+ e = now.replace(day=1).strftime('%Y-%m-%d')
143
+ resp = ce.get_cost_and_usage(TimePeriod={'Start': s, 'End': e}, Granularity='MONTHLY', Metrics=['UnblendedCost'])
144
+ total = float(resp['ResultsByTime'][0]['Total']['UnblendedCost']['Amount'])
145
+ if resources:
146
+ per_resource = total / len(resources)
147
+ for r in resources:
148
+ r.metrics['monthly_cost'] = round(per_resource, 2)
149
+ except:
150
+ pass
151
+ except:
152
+ pass
@@ -0,0 +1,118 @@
1
+ from __future__ import annotations
2
+ """Evaluation engine — applies policies against resources."""
3
+
4
+ import uuid
5
+ from datetime import datetime
6
+ from ..models import Policy, Resource, Finding, Operator, Severity
7
+
8
+
9
+ def evaluate(policies: list[Policy], resources: list[Resource]) -> list[Finding]:
10
+ """Evaluate all policies against all resources. Returns findings (violations)."""
11
+ findings: list[Finding] = []
12
+
13
+ for policy in policies:
14
+ matching = _filter_by_scope(resources, policy)
15
+ for resource in matching:
16
+ violation = _check_rule(resource, policy)
17
+ if violation:
18
+ findings.append(violation)
19
+
20
+ return findings
21
+
22
+
23
+ def _filter_by_scope(resources: list[Resource], policy: Policy) -> list[Resource]:
24
+ """Filter resources by policy scope."""
25
+ scope = policy.spec.scope
26
+ result = []
27
+
28
+ for r in resources:
29
+ # Check resource type
30
+ if not _matches_list(r.resource_type, scope.resource_types):
31
+ continue
32
+ # Check region
33
+ if not _matches_list(r.region, scope.regions):
34
+ continue
35
+ # Check account
36
+ if not _matches_list(r.account_id, scope.accounts):
37
+ continue
38
+ # Check environment tag
39
+ if scope.environments != ["*"]:
40
+ env = r.tags.get("Environment", r.tags.get("env", ""))
41
+ if not _matches_list(env, scope.environments):
42
+ continue
43
+ # Check exclude
44
+ if scope.exclude:
45
+ exclude_tags = scope.exclude.get("tags", {})
46
+ if any(r.tags.get(k) == v for k, v in exclude_tags.items()):
47
+ continue
48
+ if r.resource_id in scope.exclude.get("resource_ids", []):
49
+ continue
50
+
51
+ result.append(r)
52
+
53
+ return result
54
+
55
+
56
+ def _check_rule(resource: Resource, policy: Policy) -> Finding | None:
57
+ """Check if a resource violates the policy rule."""
58
+ rule = policy.spec.rule
59
+ metric_value = resource.metrics.get(rule.metric)
60
+
61
+ if metric_value is None:
62
+ return None # No data for this metric — skip
63
+
64
+ # Apply operator
65
+ violated = _apply_operator(metric_value, rule.operator, rule.threshold)
66
+ if not violated:
67
+ return None
68
+
69
+ # Check additional conditions (AND logic)
70
+ for cond in rule.additional_conditions:
71
+ cond_value = resource.metrics.get(cond.field) or resource.properties.get(cond.field)
72
+ if cond_value is None:
73
+ return None # Missing data — skip
74
+ if not _apply_operator(float(cond_value), cond.operator, float(cond.value)):
75
+ return None # Condition not met
76
+
77
+ # Build finding
78
+ recommendation = None
79
+ estimated_savings = None
80
+ for action in policy.spec.actions:
81
+ if action.type == "recommend" and action.suggestion:
82
+ recommendation = action.suggestion
83
+ if action.estimated_savings == "calc":
84
+ estimated_savings = resource.metrics.get("monthly_cost")
85
+
86
+ return Finding(
87
+ id=str(uuid.uuid4())[:8],
88
+ policy_name=policy.metadata.name,
89
+ severity=policy.spec.severity,
90
+ resource_id=resource.resource_id,
91
+ resource_type=resource.resource_type,
92
+ region=resource.region,
93
+ account_id=resource.account_id,
94
+ message=f"{rule.metric} = {metric_value} (threshold: {rule.operator.value}{rule.threshold})",
95
+ metric_name=rule.metric,
96
+ metric_value=metric_value,
97
+ threshold=rule.threshold,
98
+ estimated_savings=estimated_savings,
99
+ recommendation=recommendation,
100
+ remediation_eligible=policy.spec.remediation.auto_eligible if policy.spec.remediation else False,
101
+ timestamp=datetime.utcnow(),
102
+ )
103
+
104
+
105
+ def _apply_operator(value: float, operator: Operator, threshold: float) -> bool:
106
+ if operator == Operator.lt: return value < threshold
107
+ elif operator == Operator.gt: return value > threshold
108
+ elif operator == Operator.lte: return value <= threshold
109
+ elif operator == Operator.gte: return value >= threshold
110
+ elif operator == Operator.eq: return value == threshold
111
+ elif operator == Operator.ne: return value != threshold
112
+ return False
113
+
114
+
115
+ def _matches_list(value: str, allowed: list[str]) -> bool:
116
+ if "*" in allowed:
117
+ return True
118
+ return value in allowed
@@ -0,0 +1,54 @@
1
+ """Load and validate YAML policy files."""
2
+
3
+ import os
4
+ import yaml
5
+ from pathlib import Path
6
+ from ..models import Policy
7
+
8
+
9
+ def load_policies(path: str) -> list[Policy]:
10
+ """Load all .yaml policy files from a directory or single file."""
11
+ policies = []
12
+ p = Path(path)
13
+
14
+ if p.is_file():
15
+ policies.append(_load_single(p))
16
+ elif p.is_dir():
17
+ for f in sorted(p.glob("*.yaml")):
18
+ policies.append(_load_single(f))
19
+ for f in sorted(p.glob("*.yml")):
20
+ policies.append(_load_single(f))
21
+ else:
22
+ raise FileNotFoundError(f"Policy path not found: {path}")
23
+
24
+ return policies
25
+
26
+
27
+ def _load_single(path: Path) -> Policy:
28
+ """Load and validate a single policy file."""
29
+ with open(path) as f:
30
+ data = yaml.safe_load(f)
31
+
32
+ if not data:
33
+ raise ValueError(f"Empty policy file: {path}")
34
+
35
+ try:
36
+ return Policy(**data)
37
+ except Exception as e:
38
+ raise ValueError(f"Invalid policy {path.name}: {e}")
39
+
40
+
41
+ def validate_policies(path: str) -> list[dict]:
42
+ """Validate policies without evaluating. Returns list of {file, valid, error}."""
43
+ results = []
44
+ p = Path(path)
45
+ files = [p] if p.is_file() else list(p.glob("*.yaml")) + list(p.glob("*.yml"))
46
+
47
+ for f in files:
48
+ try:
49
+ _load_single(f)
50
+ results.append({"file": f.name, "valid": True, "error": None})
51
+ except Exception as e:
52
+ results.append({"file": f.name, "valid": False, "error": str(e)})
53
+
54
+ return results
@@ -0,0 +1,109 @@
1
+ """Core models for SOFE — Policy, Resource, Finding."""
2
+
3
+ from pydantic import BaseModel
4
+ from typing import Optional, Union
5
+ from enum import Enum
6
+ from datetime import datetime
7
+
8
+
9
+ class Severity(str, Enum):
10
+ critical = "critical"
11
+ high = "high"
12
+ medium = "medium"
13
+ low = "low"
14
+ info = "info"
15
+
16
+
17
+ class Operator(str, Enum):
18
+ lt = "<"
19
+ gt = ">"
20
+ lte = "<="
21
+ gte = ">="
22
+ eq = "=="
23
+ ne = "!="
24
+
25
+
26
+ class AdditionalCondition(BaseModel):
27
+ field: str
28
+ operator: Operator
29
+ value: Union[float, str]
30
+
31
+
32
+ class Scope(BaseModel):
33
+ environments: list[str] = ["*"]
34
+ resource_types: list[str]
35
+ regions: list[str] = ["*"]
36
+ accounts: list[str] = ["*"]
37
+ exclude: Optional[dict] = None
38
+
39
+
40
+ class Rule(BaseModel):
41
+ metric: str
42
+ period: str = "30d"
43
+ operator: Operator
44
+ threshold: float
45
+ additional_conditions: list[AdditionalCondition] = []
46
+
47
+
48
+ class Action(BaseModel):
49
+ type: str # finding, notify, recommend, block
50
+ channel: Optional[str] = None
51
+ suggestion: Optional[str] = None
52
+ estimated_savings: Optional[str] = None
53
+
54
+
55
+ class Remediation(BaseModel):
56
+ auto_eligible: bool = False
57
+ action: Optional[str] = None
58
+ requires_approval_if: Optional[dict] = None
59
+
60
+
61
+ class PolicyMetadata(BaseModel):
62
+ name: str
63
+ description: str
64
+ author: Optional[str] = None
65
+ created: Optional[str] = None
66
+ tags: list[str] = []
67
+
68
+
69
+ class PolicySpec(BaseModel):
70
+ scope: Scope
71
+ rule: Rule
72
+ severity: Severity
73
+ actions: list[Action] = []
74
+ remediation: Optional[Remediation] = None
75
+
76
+
77
+ class Policy(BaseModel):
78
+ apiVersion: str = "sofe/v1"
79
+ kind: str = "Policy"
80
+ metadata: PolicyMetadata
81
+ spec: PolicySpec
82
+
83
+
84
+ class Resource(BaseModel):
85
+ resource_id: str
86
+ resource_type: str
87
+ region: str
88
+ account_id: str
89
+ tags: dict[str, str] = {}
90
+ properties: dict = {}
91
+ metrics: dict[str, float] = {}
92
+
93
+
94
+ class Finding(BaseModel):
95
+ id: str
96
+ policy_name: str
97
+ severity: Severity
98
+ resource_id: str
99
+ resource_type: str
100
+ region: str
101
+ account_id: str
102
+ message: str
103
+ metric_name: str
104
+ metric_value: float
105
+ threshold: float
106
+ estimated_savings: Optional[float] = None
107
+ recommendation: Optional[str] = None
108
+ remediation_eligible: bool = False
109
+ timestamp: datetime
@@ -0,0 +1,374 @@
1
+ Metadata-Version: 2.4
2
+ Name: sofe
3
+ Version: 0.1.0
4
+ Summary: Stairway Open FinOps Engine — FinOps Policies as Code for AWS
5
+ Author-email: Carlos Cortez <carlos@cortez.cloud>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/breakingthecloud/sofe
8
+ Project-URL: Repository, https://github.com/breakingthecloud/sofe
9
+ Project-URL: Issues, https://github.com/breakingthecloud/sofe/issues
10
+ Keywords: finops,aws,cloud-governance,policy-as-code,cost-optimization
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: System :: Systems Administration
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ Requires-Dist: boto3>=1.34
21
+ Requires-Dist: pydantic>=2.0
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: click>=8.0
24
+ Requires-Dist: rich>=13.0
25
+
26
+ # šŸ—ļø SOFE — Stairway Open FinOps Engine
27
+
28
+ **FinOps Policies as Code for AWS.**
29
+
30
+ SOFE evaluates declarative YAML policies against live AWS infrastructure and produces actionable findings — idle resources, missing tags, governance violations, and cost savings opportunities.
31
+
32
+ ```bash
33
+ sofe evaluate --policies ./policies/ --profile production
34
+ ```
35
+
36
+ ```
37
+ ────────────────────────────────────────────────────────────────────────────────
38
+ Severity Policy Resource Message
39
+ ────────────────────────────────────────────────────────────────────────────────
40
+ 🟠 high no-idle-ec2 i-0abc123def avg_cpu = 2.1% (threshold: <5%)
41
+ 🟔 medium require-cost-tags i-0def456ghi missing: costCenter, owner
42
+ 🟔 medium no-unattached-ebs vol-789abc 180 days old, 500GB
43
+ ────────────────────────────────────────────────────────────────────────────────
44
+ Summary: 3 findings | Potential savings: $365.00/mo
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Why SOFE?
50
+
51
+ ### The Problem
52
+
53
+ Teams today manage cloud costs **reactively** — they see the bill spike, panic, then scramble to find what changed. Existing tools either:
54
+
55
+ - **Alert on total spend** (AWS Budgets) — no root cause, no policy enforcement
56
+ - **Scan for security** (Prowler, ScoutSuite) — not cost-focused
57
+ - **Estimate costs** (Infracost) — pre-deploy only, no runtime enforcement
58
+ - **Lock you in** (Sentinel/HCP) — vendor-specific, not portable
59
+
60
+ **No tool does:** declarative cost+governance policies that evaluate against **live** infrastructure and produce findings with dollar-amount savings.
61
+
62
+ ### The Solution
63
+
64
+ SOFE fills this gap:
65
+
66
+ ```yaml
67
+ # policies/no-idle-production.yaml
68
+ apiVersion: sofe/v1
69
+ kind: Policy
70
+ metadata:
71
+ name: no-idle-production
72
+ description: "Flag idle EC2 in production (< 5% CPU for 30 days)"
73
+ spec:
74
+ scope:
75
+ environments: [production]
76
+ resource_types: [aws.ec2]
77
+ rule:
78
+ metric: avg_cpu_utilization
79
+ period: 30d
80
+ operator: "<"
81
+ threshold: 5
82
+ severity: high
83
+ actions:
84
+ - type: recommend
85
+ suggestion: "Rightsize or terminate"
86
+ estimated_savings: calc
87
+ ```
88
+
89
+ Write a policy once. Run it daily. Get findings with savings.
90
+
91
+ ---
92
+
93
+ ## Who Should Use SOFE?
94
+
95
+ | Role | Why SOFE matters |
96
+ |------|-----------------|
97
+ | **Cloud/DevOps Engineers** | Automate governance checks in CI/CD. `sofe evaluate --fail-on high` blocks deploys that violate cost policies. |
98
+ | **FinOps Practitioners** | Define cost optimization rules as code. Track compliance across accounts. Quantify waste. |
99
+ | **Platform Engineers** | Enforce tagging standards, idle resource cleanup, and architecture best practices at scale. |
100
+ | **CTOs / Engineering Managers** | Visibility into cloud waste without manual audits. "We save $X/month because of these policies." |
101
+ | **AWS Partners / Consultants** | Deliver FinOps assessments faster with repeatable, auditable policy evaluations. |
102
+
103
+ ---
104
+
105
+ ## Why SOFE is Key for FinOps + Governance
106
+
107
+ ### 1. FinOps: Cost Optimization as Code
108
+
109
+ Traditional FinOps is manual: someone opens Cost Explorer, finds waste, creates a ticket. SOFE automates this:
110
+
111
+ ```
112
+ Write policy → sofe evaluate → findings with $ savings → action
113
+ ```
114
+
115
+ Every policy produces **quantified savings**: "$340/mo if you terminate this idle instance."
116
+
117
+ ### 2. Cloud Governance: Policies that Actually Enforce
118
+
119
+ Tags, encryption, public access, budget limits — every team has rules but no enforcement. SOFE makes them executable:
120
+
121
+ ```yaml
122
+ - require-cost-tags → "All resources must have owner + costCenter"
123
+ - s3-encryption-required → "All S3 buckets must have encryption enabled"
124
+ - no-public-without-waf → "No public-facing resource without WAF"
125
+ ```
126
+
127
+ Not just documentation. Actual enforcement in CI/CD.
128
+
129
+ ### 3. DevOps: Shift-Left Cost Awareness
130
+
131
+ Add `sofe evaluate --fail-on high` to your GitHub Action or CI pipeline. Developers see cost violations **before** merge, not after the bill arrives.
132
+
133
+ ### 4. BYaML Integration: Architecture-Aware FinOps
134
+
135
+ SOFE uses [BYaML](https://byaml.org) component types (`aws.ec2`, `aws.s3`, etc.) — the same type system used for architecture governance. This means:
136
+
137
+ - Policies reference the same types as your architecture definitions
138
+ - Findings map directly to BYaML components
139
+ - Cost data correlates with architecture versions
140
+
141
+ ---
142
+
143
+ ## Quick Start
144
+
145
+ ### Install
146
+
147
+ ```bash
148
+ pip install sofe
149
+ ```
150
+
151
+ ### Write Your First Policy
152
+
153
+ ```yaml
154
+ # policies/require-tags.yaml
155
+ apiVersion: sofe/v1
156
+ kind: Policy
157
+ metadata:
158
+ name: require-cost-tags
159
+ description: "All resources must have owner and costCenter tags"
160
+ spec:
161
+ scope:
162
+ resource_types: [aws.ec2, aws.rds, aws.s3]
163
+ rule:
164
+ metric: has_tag:owner
165
+ operator: "=="
166
+ threshold: 0
167
+ severity: medium
168
+ actions:
169
+ - type: finding
170
+ ```
171
+
172
+ ### Validate
173
+
174
+ ```bash
175
+ sofe validate --policies ./policies/
176
+ ```
177
+
178
+ ### Evaluate
179
+
180
+ ```bash
181
+ # Against real AWS (uses your AWS profile)
182
+ sofe evaluate --policies ./policies/ --profile production
183
+
184
+ # Output as JSON (for automation)
185
+ sofe evaluate --policies ./policies/ --format json > findings.json
186
+
187
+ # CI/CD mode (exit code 1 if high/critical found)
188
+ sofe evaluate --policies ./policies/ --fail-on high
189
+ ```
190
+
191
+ ---
192
+
193
+ ## How It Works
194
+
195
+ ```
196
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
197
+ │ Policy Loader │ │ Collectors │ │ Evaluation Engine │
198
+ │ │ │ │ │ │
199
+ │ Reads YAML │────▶│ AWS APIs: │────▶│ For each policy: │
200
+ │ Validates │ │ EC2, RDS │ │ match scope → │
201
+ │ schema │ │ S3, Lambda │ │ evaluate condition → │
202
+ │ │ │ CloudWatch │ │ if violated → │
203
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ generate finding │
204
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
205
+ │
206
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā–¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
207
+ │ Output │
208
+ │ • Table (CLI) │
209
+ │ • JSON (CI/CD) │
210
+ │ • Markdown (PRs) │
211
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
212
+ ```
213
+
214
+ ---
215
+
216
+ ## Supported Metrics
217
+
218
+ | Metric | Source | Resources |
219
+ |--------|--------|-----------|
220
+ | `avg_cpu_utilization` | CloudWatch (30d avg) | EC2, RDS |
221
+ | `monthly_cost` | Cost Explorer | All |
222
+ | `running_days` | LaunchTime | EC2, RDS |
223
+ | `has_tag:{key}` | Tags API | All |
224
+ | `storage_used_gb` | CloudWatch | S3, EBS |
225
+ | `connections` | CloudWatch | RDS |
226
+ | `invocations` | CloudWatch | Lambda |
227
+
228
+ ---
229
+
230
+ ## Pre-Built Policies
231
+
232
+ | Policy | Type | Severity |
233
+ |--------|------|----------|
234
+ | `no-idle-ec2` | Cost Optimization | high |
235
+ | `no-idle-rds` | Cost Optimization | high |
236
+ | `require-cost-tags` | Governance | medium |
237
+ | `no-oversized-staging` | Cost Optimization | high |
238
+ | `s3-lifecycle-required` | Storage | medium |
239
+ | `s3-encryption-required` | Security/Cost | high |
240
+ | `no-unattached-ebs` | Storage | medium |
241
+ | `no-old-snapshots` | Storage | low |
242
+ | `budget-exceeded` | Budget | critical |
243
+ | `no-public-without-waf` | Security/Cost | high |
244
+
245
+ ---
246
+
247
+ ## CI/CD Integration
248
+
249
+ ### GitHub Actions
250
+
251
+ ```yaml
252
+ - name: FinOps Policy Check
253
+ run: |
254
+ pip install sofe
255
+ sofe evaluate --policies ./policies/ --fail-on high --format json > findings.json
256
+ ```
257
+
258
+ ### Exit Codes
259
+
260
+ | Code | Meaning |
261
+ |:----:|---------|
262
+ | 0 | No violations (or below `--fail-on` threshold) |
263
+ | 1 | Violations found at or above `--fail-on` severity |
264
+
265
+ ---
266
+
267
+ ## Comparison
268
+
269
+ | Tool | Cost Policies | Live Eval | Savings Calc | CI/CD | Open Source |
270
+ |------|:---:|:---:|:---:|:---:|:---:|
271
+ | **SOFE** | āœ… | āœ… | āœ… | āœ… | āœ… |
272
+ | AWS Budgets | āŒ (alerts only) | āŒ | āŒ | āŒ | āŒ |
273
+ | Infracost | 🟔 (pre-deploy) | āŒ | āœ… | āœ… | āœ… |
274
+ | OPA/Rego | āœ… (security) | āŒ | āŒ | āœ… | āœ… |
275
+ | Sentinel | āœ… | āŒ | āŒ | āœ… | āŒ (HCP only) |
276
+ | Prowler | āŒ (security only) | āœ… | āŒ | āœ… | āœ… |
277
+
278
+ ---
279
+
280
+ ## Ecosystem: Competitors & Complementary Tools
281
+
282
+ ### Competitors (overlap with SOFE)
283
+
284
+ | Tool | Type | What it does | How SOFE differs |
285
+ |------|------|-------------|-----------------|
286
+ | **OPA / Rego** | OSS | General policy engine (security-focused) | SOFE is cost/FinOps-focused with savings calculations. OPA doesn't calculate $. |
287
+ | **HashiCorp Sentinel** | Proprietary | Policy-as-code for Terraform | Locked to HCP/Terraform Cloud. SOFE is runtime (evaluates live infra, not just plans). |
288
+ | **Infracost** | OSS | Cost estimation pre-deploy | Pre-deploy only. SOFE evaluates running infra + historical drift. Complementary. |
289
+ | **AWS Config Rules** | AWS Native | Compliance rules on AWS resources | Limited to AWS, no cost focus, no CI/CD output, no portability. |
290
+ | **Prowler** | OSS | Security & compliance scanning | Security-focused (CIS, PCI-DSS). Doesn't calculate cost savings or enforce FinOps. |
291
+ | **Checkov** | OSS | IaC static analysis (Terraform, CF) | Pre-deploy only (scans .tf files). SOFE scans live resources. |
292
+ | **Cloud Custodian** | OSS | Policy engine for cloud resources | Closest competitor. Actions (stop/terminate) built-in. SOFE is lighter, YAML-first, FinOps-focused. |
293
+ | **Kubecost** | OSS/Paid | Kubernetes cost monitoring | K8s only. SOFE covers all AWS services. |
294
+ | **Vantage** | SaaS | FinOps dashboard + alerts | Dashboard, not policy engine. No CI/CD. No custom rules. |
295
+ | **CloudZero** | SaaS | Cost intelligence platform | Enterprise SaaS ($$$). No self-hosted. No policies-as-code. |
296
+ | **Spot.io / NetApp** | SaaS | Cloud optimization + autoscaling | Optimization execution, not policy definition. Complementary. |
297
+ | **Apptio Cloudability** | SaaS | Enterprise FinOps platform | Enterprise-only, expensive. No CI/CD integration. No code-first approach. |
298
+ | **nOps** | SaaS | AWS cost optimization + scheduling | Automation focus. No declarative policies. |
299
+ | **CAST AI** | SaaS | K8s cost optimization | K8s-only autoscaling. Not a policy engine. |
300
+
301
+ ### Complementary Tools (use alongside SOFE)
302
+
303
+ | Tool | How it complements SOFE |
304
+ |------|------------------------|
305
+ | **Infracost** | Pre-deploy cost estimation → SOFE catches what slipped through post-deploy |
306
+ | **Terraform / OpenTofu** | Defines infra → SOFE evaluates if running infra matches cost policies |
307
+ | **AWS Cost Explorer** | Data source → SOFE collectors fetch from it |
308
+ | **CloudWatch** | Metrics source → SOFE uses CPU, connections, invocations |
309
+ | **Steampipe** | SQL-based cloud inventory → could be alternate data source for SOFE |
310
+ | **Prometheus + Grafana** | Monitoring → SOFE could consume Prometheus metrics (future) |
311
+ | **BYaML** | Architecture definitions → SOFE policies use same type system |
312
+ | **byaml-finops-mcp** | MCP tools for AI assistants → SOFE findings feed into AI reasoning |
313
+ | **FinOptix** | AI model for FinOps → explains SOFE findings in natural language |
314
+ | **GitHub Actions / GitLab CI** | CI/CD → SOFE runs as pipeline step with `--fail-on` |
315
+ | **Slack / PagerDuty** | Notifications → SOFE can webhook findings (future) |
316
+ | **Neo4j** | Graph DB → SOFE findings + BYaML relationships = cost propagation graph (future) |
317
+
318
+ ### The SOFE Position
319
+
320
+ ```
321
+ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
322
+ │ Cloud Cost Lifecycle │
323
+ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
324
+ │ PLAN │ DEPLOY │ RUN │ OPTIMIZE │
325
+ │ │ │ │ │
326
+ │ Infracost │ Sentinel │ ā˜… SOFE ā˜… │ Spot.io │
327
+ │ Checkov │ OPA/Rego │ Cloud Custodian │ CAST AI │
328
+ │ │ Checkov │ AWS Config │ nOps │
329
+ │ │ │ Prowler │ Kubecost │
330
+ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
331
+ │ VISIBILITY: Vantage, CloudZero, Apptio, AWS Cost Explorer │
332
+ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
333
+
334
+ SOFE lives in the RUN phase: evaluate LIVE infrastructure against
335
+ declarative FinOps policies. Produce findings with dollar savings.
336
+ ```
337
+
338
+ ### Why SOFE vs Cloud Custodian?
339
+
340
+ Cloud Custodian is the closest open source alternative. Key differences:
341
+
342
+ | | SOFE | Cloud Custodian |
343
+ |--|------|----------------|
344
+ | **Focus** | FinOps + cost governance | Security + compliance + ops |
345
+ | **Policy format** | Clean YAML (Pydantic-validated) | Complex YAML with filters/actions DSL |
346
+ | **Savings calculation** | Built-in ($ per finding) | Not included |
347
+ | **BYaML integration** | Native (same type system) | None |
348
+ | **AI reasoning** | FinOptix integration (future) | None |
349
+ | **CI/CD** | `--fail-on` exit code | Requires wrapper |
350
+ | **Scope** | AWS first, multi-cloud future | AWS + Azure + GCP |
351
+ | **Maturity** | New (2026) | Mature (2016+, Capital One) |
352
+
353
+ SOFE is opinionated toward **FinOps** — every finding has a dollar amount. Cloud Custodian is a general-purpose policy engine that happens to work on cloud resources.
354
+
355
+ ---
356
+
357
+ ## License
358
+
359
+ Apache 2.0 — free to use, modify, and distribute.
360
+
361
+ ---
362
+
363
+ ## Contributing
364
+
365
+ 1. Fork the repo
366
+ 2. Add a policy to `policies/` or a collector to `sofe/collectors/`
367
+ 3. Submit a PR
368
+
369
+ ---
370
+
371
+ ## Built by
372
+
373
+ [Carlos Cortez](https://cortez.cloud) — AWS Community Hero, CTO @ BWIT Solutions.
374
+ Part of the [BYaML](https://byaml.org) ecosystem for cloud architecture governance.
@@ -0,0 +1,11 @@
1
+ sofe/__init__.py,sha256=lBrL-ij6gZimgN3fPaVJPU2fZNzurfBtMLzYgmMeGbw,92
2
+ sofe/cli.py,sha256=qjeY504zxfS8S8Kudu1TCBZxua9AX3ag_q8BqZHij4Q,4630
3
+ sofe/collectors/__init__.py,sha256=s65VhKot726K7IcWF_n6CkNQ-jMNRHAWINgMtw0F7EU,6355
4
+ sofe/engine/__init__.py,sha256=4l7RrJeIgCHdacNrVtnW3xbqkOi_qGseiUwUXTWQXWw,4258
5
+ sofe/loader/__init__.py,sha256=4k6ygsZ-0HFzcfZ2TJsTdQjpdNhOxM9s6ZgHp2Rgjgw,1503
6
+ sofe/models/__init__.py,sha256=PvfA6BFMyl9bviw8TumZkii4QqtR6ysCMX5ttUFUJM8,2277
7
+ sofe-0.1.0.dist-info/METADATA,sha256=XUWxXLWt67mphgbQp2VooTAN0DCBLvzr8iYFGSa2kZk,16061
8
+ sofe-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
9
+ sofe-0.1.0.dist-info/entry_points.txt,sha256=QOQi7OO-5oQUr2mJubvIVKmWusSj2D2SmPkk6iYt35Y,39
10
+ sofe-0.1.0.dist-info/top_level.txt,sha256=GCmO2On-zrWOiECzxOWg-dfI2hbObe0kddQjvuNET94,5
11
+ sofe-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sofe = sofe.cli:main
@@ -0,0 +1 @@
1
+ sofe