cyntrisec 0.1.7__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.
Files changed (65) hide show
  1. cyntrisec/__init__.py +3 -0
  2. cyntrisec/__main__.py +6 -0
  3. cyntrisec/aws/__init__.py +6 -0
  4. cyntrisec/aws/collectors/__init__.py +17 -0
  5. cyntrisec/aws/collectors/ec2.py +30 -0
  6. cyntrisec/aws/collectors/iam.py +116 -0
  7. cyntrisec/aws/collectors/lambda_.py +45 -0
  8. cyntrisec/aws/collectors/network.py +70 -0
  9. cyntrisec/aws/collectors/rds.py +38 -0
  10. cyntrisec/aws/collectors/s3.py +68 -0
  11. cyntrisec/aws/collectors/usage.py +188 -0
  12. cyntrisec/aws/credentials.py +153 -0
  13. cyntrisec/aws/normalizers/__init__.py +17 -0
  14. cyntrisec/aws/normalizers/ec2.py +115 -0
  15. cyntrisec/aws/normalizers/iam.py +182 -0
  16. cyntrisec/aws/normalizers/lambda_.py +83 -0
  17. cyntrisec/aws/normalizers/network.py +225 -0
  18. cyntrisec/aws/normalizers/rds.py +130 -0
  19. cyntrisec/aws/normalizers/s3.py +184 -0
  20. cyntrisec/aws/relationship_builder.py +1359 -0
  21. cyntrisec/aws/scanner.py +303 -0
  22. cyntrisec/cli/__init__.py +5 -0
  23. cyntrisec/cli/analyze.py +747 -0
  24. cyntrisec/cli/ask.py +412 -0
  25. cyntrisec/cli/can.py +307 -0
  26. cyntrisec/cli/comply.py +226 -0
  27. cyntrisec/cli/cuts.py +231 -0
  28. cyntrisec/cli/diff.py +332 -0
  29. cyntrisec/cli/errors.py +105 -0
  30. cyntrisec/cli/explain.py +348 -0
  31. cyntrisec/cli/main.py +114 -0
  32. cyntrisec/cli/manifest.py +893 -0
  33. cyntrisec/cli/output.py +117 -0
  34. cyntrisec/cli/remediate.py +643 -0
  35. cyntrisec/cli/report.py +462 -0
  36. cyntrisec/cli/scan.py +207 -0
  37. cyntrisec/cli/schemas.py +391 -0
  38. cyntrisec/cli/serve.py +164 -0
  39. cyntrisec/cli/setup.py +260 -0
  40. cyntrisec/cli/validate.py +101 -0
  41. cyntrisec/cli/waste.py +323 -0
  42. cyntrisec/core/__init__.py +31 -0
  43. cyntrisec/core/business_config.py +110 -0
  44. cyntrisec/core/business_logic.py +131 -0
  45. cyntrisec/core/compliance.py +437 -0
  46. cyntrisec/core/cost_estimator.py +301 -0
  47. cyntrisec/core/cuts.py +360 -0
  48. cyntrisec/core/diff.py +361 -0
  49. cyntrisec/core/graph.py +202 -0
  50. cyntrisec/core/paths.py +830 -0
  51. cyntrisec/core/schema.py +317 -0
  52. cyntrisec/core/simulator.py +371 -0
  53. cyntrisec/core/waste.py +309 -0
  54. cyntrisec/mcp/__init__.py +5 -0
  55. cyntrisec/mcp/server.py +862 -0
  56. cyntrisec/storage/__init__.py +7 -0
  57. cyntrisec/storage/filesystem.py +344 -0
  58. cyntrisec/storage/memory.py +113 -0
  59. cyntrisec/storage/protocol.py +92 -0
  60. cyntrisec-0.1.7.dist-info/METADATA +672 -0
  61. cyntrisec-0.1.7.dist-info/RECORD +65 -0
  62. cyntrisec-0.1.7.dist-info/WHEEL +4 -0
  63. cyntrisec-0.1.7.dist-info/entry_points.txt +2 -0
  64. cyntrisec-0.1.7.dist-info/licenses/LICENSE +190 -0
  65. cyntrisec-0.1.7.dist-info/licenses/NOTICE +5 -0
@@ -0,0 +1,309 @@
1
+ """
2
+ Waste Analyzer - Identify unused IAM capabilities for blast radius reduction.
3
+
4
+ Analyzes the gap between "permissions granted" and "permissions used"
5
+ to find waste that increases attack surface without providing value.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from datetime import datetime, timedelta
12
+
13
+ from cyntrisec.core.schema import Asset
14
+
15
+
16
+ @dataclass
17
+ class UnusedCapability:
18
+ """
19
+ An unused permission that contributes to blast radius.
20
+
21
+ Attributes:
22
+ role_arn: The IAM role with this capability
23
+ role_name: Human-readable role name
24
+ service: AWS service namespace (e.g., 's3', 'iam')
25
+ service_name: Full service name (e.g., 'Amazon S3')
26
+ days_unused: Number of days since last use (None = never used)
27
+ risk_level: 'critical', 'high', 'medium', 'low'
28
+ recommendation: What to do about it
29
+ """
30
+
31
+ role_arn: str
32
+ role_name: str
33
+ service: str
34
+ service_name: str
35
+ days_unused: int | None = None # None means never used
36
+ risk_level: str = "medium"
37
+ recommendation: str = ""
38
+
39
+ @property
40
+ def never_used(self) -> bool:
41
+ return self.days_unused is None
42
+
43
+
44
+ @dataclass
45
+ class WasteReport:
46
+ """
47
+ Summary of unused capabilities in an AWS account.
48
+
49
+ Attributes:
50
+ role_reports: Waste analysis per role
51
+ total_unused: Total count of unused service permissions
52
+ blast_radius_reduction: Estimated % reduction if cleaned up
53
+ """
54
+
55
+ role_reports: list[RoleWasteReport] = field(default_factory=list)
56
+ total_unused: int = 0
57
+ total_permissions: int = 0
58
+ analyzed_at: datetime = field(default_factory=datetime.utcnow)
59
+
60
+ @property
61
+ def blast_radius_reduction(self) -> float:
62
+ """Percentage of permissions that are unused."""
63
+ if self.total_permissions == 0:
64
+ return 0.0
65
+ return self.total_unused / self.total_permissions
66
+
67
+
68
+ @dataclass
69
+ class RoleWasteReport:
70
+ """Waste analysis for a single IAM role."""
71
+
72
+ role_arn: str
73
+ role_name: str
74
+ unused_capabilities: list[UnusedCapability] = field(default_factory=list)
75
+ total_services: int = 0
76
+ unused_services: int = 0
77
+
78
+ @property
79
+ def blast_radius_reduction(self) -> float:
80
+ """Percentage reduction if unused capabilities removed."""
81
+ if self.total_services == 0:
82
+ return 0.0
83
+ return self.unused_services / self.total_services
84
+
85
+
86
+ # High-risk services that should be flagged if unused
87
+ HIGH_RISK_SERVICES = {
88
+ "iam": "critical", # Identity management
89
+ "sts": "high", # Token service
90
+ "kms": "high", # Encryption keys
91
+ "secretsmanager": "high", # Secrets
92
+ "ssm": "high", # Systems Manager
93
+ "ec2": "medium", # Compute
94
+ "s3": "medium", # Storage
95
+ "lambda": "medium", # Functions
96
+ "rds": "medium", # Databases
97
+ "dynamodb": "medium", # NoSQL
98
+ }
99
+
100
+
101
+ class WasteAnalyzer:
102
+ """
103
+ Analyze IAM permissions for unused capabilities.
104
+
105
+ Compares granted permissions against actual usage to find
106
+ opportunities for blast radius reduction.
107
+ """
108
+
109
+ def __init__(self, days_threshold: int = 90):
110
+ """
111
+ Initialize analyzer.
112
+
113
+ Args:
114
+ days_threshold: Consider unused if not accessed in this many days
115
+ """
116
+ self.days_threshold = days_threshold
117
+ self._cutoff = datetime.utcnow() - timedelta(days=days_threshold)
118
+
119
+ def _is_aws_managed_role(self, role: Asset) -> bool:
120
+ """Check if role is AWS-managed and should be excluded from waste analysis."""
121
+ name = role.name or ""
122
+ arn = role.arn or role.aws_resource_id or ""
123
+
124
+ # AWS service-linked roles
125
+ if name.startswith("AWSServiceRole"):
126
+ return True
127
+ if name.startswith("AWSReservedSSO_"):
128
+ return True
129
+ if "/aws-service-role/" in arn:
130
+ return True
131
+
132
+ return False
133
+
134
+ def analyze_from_usage_reports(
135
+ self,
136
+ usage_reports: list[any], # List[RoleUsageReport]
137
+ ) -> WasteReport:
138
+ """
139
+ Analyze usage reports to find waste.
140
+
141
+ Args:
142
+ usage_reports: List of RoleUsageReport from UsageCollector
143
+
144
+ Returns:
145
+ WasteReport with all unused capabilities
146
+ """
147
+ report = WasteReport()
148
+
149
+ for usage in usage_reports:
150
+ role_waste = self._analyze_role(usage)
151
+ report.role_reports.append(role_waste)
152
+ report.total_unused += role_waste.unused_services
153
+ report.total_permissions += role_waste.total_services
154
+
155
+ return report
156
+
157
+ def _analyze_role(self, usage) -> RoleWasteReport:
158
+ """Analyze a single role's usage."""
159
+ role_waste = RoleWasteReport(
160
+ role_arn=usage.role_arn,
161
+ role_name=usage.role_name,
162
+ total_services=len(usage.services),
163
+ )
164
+
165
+ for svc in usage.services:
166
+ # Check if unused (never accessed or not accessed within threshold)
167
+ is_unused = False
168
+ days_unused = None
169
+
170
+ if svc.last_authenticated is None:
171
+ is_unused = True
172
+ days_unused = None # Never used
173
+ else:
174
+ # Handle timezone-aware datetimes
175
+ last_auth = svc.last_authenticated
176
+ if hasattr(last_auth, "replace"):
177
+ last_auth = last_auth.replace(tzinfo=None)
178
+
179
+ if last_auth < self._cutoff:
180
+ is_unused = True
181
+ days_unused = (datetime.utcnow() - last_auth).days
182
+
183
+ if is_unused:
184
+ role_waste.unused_services += 1
185
+
186
+ # Determine risk level
187
+ namespace = svc.service_namespace.lower()
188
+ risk_level = HIGH_RISK_SERVICES.get(namespace, "low")
189
+
190
+ # Build recommendation
191
+ if days_unused is None:
192
+ recommendation = f"Remove {svc.service_name} access - never used"
193
+ else:
194
+ recommendation = (
195
+ f"Remove {svc.service_name} access - unused for {days_unused} days"
196
+ )
197
+
198
+ role_waste.unused_capabilities.append(
199
+ UnusedCapability(
200
+ role_arn=usage.role_arn,
201
+ role_name=usage.role_name,
202
+ service=namespace,
203
+ service_name=svc.service_name,
204
+ days_unused=days_unused,
205
+ risk_level=risk_level,
206
+ recommendation=recommendation,
207
+ )
208
+ )
209
+
210
+ # Sort by risk level
211
+ risk_order = {"critical": 0, "high": 1, "medium": 2, "low": 3}
212
+ role_waste.unused_capabilities.sort(
213
+ key=lambda x: (risk_order.get(x.risk_level, 4), x.service)
214
+ )
215
+
216
+ return role_waste
217
+
218
+ def analyze_from_assets(
219
+ self,
220
+ assets: list[Asset],
221
+ usage_reports: list[any] | None = None,
222
+ ) -> WasteReport:
223
+ """
224
+ Analyze assets for potential waste.
225
+
226
+ This is a simpler analysis that doesn't require live AWS access.
227
+ It looks at attack paths and identifies roles that only appear
228
+ in attack paths (pure attack surface, no legitimate use).
229
+
230
+ Args:
231
+ assets: Assets from a scan
232
+ usage_reports: Optional live usage data
233
+
234
+ Returns:
235
+ WasteReport
236
+ """
237
+ if usage_reports:
238
+ return self.analyze_from_usage_reports(usage_reports)
239
+
240
+ # Fallback: offline analysis based on attached policies
241
+ report = WasteReport()
242
+
243
+ # Find IAM roles, excluding AWS-managed service roles
244
+ roles = [
245
+ a for a in assets
246
+ if a.asset_type == "iam:role" and not self._is_aws_managed_role(a)
247
+ ]
248
+
249
+ for role in roles:
250
+ role_waste = RoleWasteReport(
251
+ role_arn=role.arn or role.aws_resource_id,
252
+ role_name=role.name,
253
+ )
254
+
255
+ policy_docs = role.properties.get("policy_documents", [])
256
+ services_granted = self._extract_services_from_policies(policy_docs)
257
+ role_waste.total_services = len(services_granted)
258
+
259
+ if services_granted:
260
+ role_waste.unused_capabilities.append(
261
+ UnusedCapability(
262
+ role_arn=role.arn or role.aws_resource_id,
263
+ role_name=role.name,
264
+ service="unknown",
265
+ service_name="Usage data unavailable",
266
+ days_unused=None,
267
+ risk_level="info",
268
+ recommendation="Use --live for accurate usage analysis",
269
+ )
270
+ )
271
+
272
+ if role_waste.unused_capabilities:
273
+ report.role_reports.append(role_waste)
274
+ report.total_unused += role_waste.unused_services
275
+ report.total_permissions += role_waste.total_services
276
+
277
+ return report
278
+
279
+ def _extract_services_from_policies(self, policy_docs: list[dict]) -> list[str]:
280
+ """Extract service namespaces from policy documents."""
281
+ services: set[str] = set()
282
+ wildcard = False
283
+ for policy in policy_docs or []:
284
+ statements = policy.get("Statement", [])
285
+ if isinstance(statements, dict):
286
+ statements = [statements]
287
+ for statement in statements:
288
+ if statement.get("Effect") != "Allow":
289
+ continue
290
+ actions = statement.get("Action")
291
+ if not actions:
292
+ continue
293
+ if isinstance(actions, str):
294
+ actions = [actions]
295
+ for action in actions:
296
+ if not isinstance(action, str):
297
+ continue
298
+ if action == "*":
299
+ wildcard = True
300
+ continue
301
+ if ":" in action:
302
+ service = action.split(":", 1)[0]
303
+ if service and service != "*":
304
+ services.add(service.lower())
305
+ elif action:
306
+ services.add(action.lower())
307
+ if wildcard and not services:
308
+ services.update(HIGH_RISK_SERVICES.keys())
309
+ return sorted(services)
@@ -0,0 +1,5 @@
1
+ """MCP (Model Context Protocol) server for AI agent integration."""
2
+
3
+ from cyntrisec.mcp.server import create_mcp_server, run_mcp_server
4
+
5
+ __all__ = ["create_mcp_server", "run_mcp_server"]