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,437 @@
1
+ """
2
+ Compliance Mapping - Map findings to compliance frameworks.
3
+
4
+ Supports:
5
+ - CIS AWS Foundations Benchmark v1.5
6
+ - SOC 2 Type II controls
7
+
8
+ Each finding type is mapped to relevant compliance controls,
9
+ allowing users to understand their compliance posture.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from dataclasses import dataclass, field
15
+ from enum import Enum
16
+
17
+ from cyntrisec.core.schema import Asset, Finding
18
+
19
+
20
+ class Framework(str, Enum):
21
+ """Supported compliance frameworks."""
22
+
23
+ CIS_AWS = "CIS-AWS"
24
+ SOC2 = "SOC2"
25
+
26
+
27
+ @dataclass
28
+ class Control:
29
+ """A compliance control."""
30
+
31
+ id: str
32
+ framework: Framework
33
+ title: str
34
+ description: str
35
+ severity: str = "medium"
36
+
37
+ @property
38
+ def full_id(self) -> str:
39
+ return f"{self.framework.value}:{self.id}"
40
+
41
+
42
+ @dataclass
43
+ class ControlMapping:
44
+ """Mapping between a finding type and compliance controls."""
45
+
46
+ finding_type: str
47
+ controls: list[Control] = field(default_factory=list)
48
+
49
+
50
+ @dataclass
51
+ class ComplianceResult:
52
+ """Result of compliance check for a single control."""
53
+
54
+ control: Control
55
+ status: str # "pass", "fail", "unknown"
56
+ findings: list[Finding] = field(default_factory=list)
57
+ assets_affected: int = 0
58
+
59
+ @property
60
+ def is_passing(self) -> bool:
61
+ return self.status == "pass"
62
+
63
+
64
+ @dataclass
65
+ class ComplianceReport:
66
+ """Full compliance report for a framework."""
67
+
68
+ framework: Framework
69
+ results: list[ComplianceResult] = field(default_factory=list)
70
+ data_gaps: dict[str, dict] = field(default_factory=dict)
71
+
72
+ @property
73
+ def passing(self) -> int:
74
+ return sum(1 for r in self.results if r.status == "pass")
75
+
76
+ @property
77
+ def failing(self) -> int:
78
+ return sum(1 for r in self.results if r.status == "fail")
79
+
80
+ @property
81
+ def unknown(self) -> int:
82
+ return sum(1 for r in self.results if r.status not in {"pass", "fail"})
83
+
84
+ @property
85
+ def compliance_score(self) -> float:
86
+ """Percentage of controls passing."""
87
+ total = self.passing + self.failing
88
+ return self.passing / total if total > 0 else 0.0
89
+
90
+
91
+ CONTROL_ASSET_REQUIREMENTS: dict[str, list[str]] = {
92
+ # IAM
93
+ "CIS-AWS:1.4": ["iam:user"],
94
+ "CIS-AWS:1.5": ["iam:user"],
95
+ "CIS-AWS:1.10": ["iam:user"],
96
+ "CIS-AWS:1.12": ["iam:user"],
97
+ "CIS-AWS:1.16": ["iam:user"],
98
+ "CIS-AWS:1.17": ["iam:role"],
99
+ # S3
100
+ "CIS-AWS:2.1.1": ["s3:bucket"],
101
+ "CIS-AWS:2.1.2": ["s3:bucket"],
102
+ "CIS-AWS:2.1.5": ["s3:bucket"],
103
+ # EC2/VPC
104
+ "CIS-AWS:5.1": ["ec2:security-group"],
105
+ "CIS-AWS:5.2": ["ec2:security-group"],
106
+ "CIS-AWS:5.3": ["ec2:vpc"],
107
+ "CIS-AWS:5.4": ["ec2:instance"],
108
+ # SOC2
109
+ "SOC2:CC6.1": ["iam:user", "iam:role"],
110
+ "SOC2:CC6.2": ["iam:user"],
111
+ "SOC2:CC6.3": ["iam:role"],
112
+ "SOC2:CC6.6": ["s3:bucket"],
113
+ "SOC2:CC7.1": ["ec2:security-group", "s3:bucket"],
114
+ "SOC2:CC7.2": ["iam:role"],
115
+ "SOC2:CC6.7": ["s3:bucket"],
116
+ }
117
+
118
+
119
+ # CIS AWS Foundations Benchmark v1.5 Controls
120
+ CIS_CONTROLS = [
121
+ # IAM
122
+ Control(
123
+ "1.4",
124
+ Framework.CIS_AWS,
125
+ "Ensure no root account access key exists",
126
+ "The root account should not have access keys configured",
127
+ "critical",
128
+ ),
129
+ Control(
130
+ "1.5",
131
+ Framework.CIS_AWS,
132
+ "Ensure MFA is enabled for root account",
133
+ "The root account should have MFA enabled",
134
+ "critical",
135
+ ),
136
+ Control(
137
+ "1.10",
138
+ Framework.CIS_AWS,
139
+ "Ensure MFA is enabled for all IAM users with console password",
140
+ "All IAM users with console access should have MFA enabled",
141
+ "high",
142
+ ),
143
+ Control(
144
+ "1.12",
145
+ Framework.CIS_AWS,
146
+ "Ensure credentials unused for 90 days are disabled",
147
+ "IAM credentials not used in 90 days should be disabled",
148
+ "medium",
149
+ ),
150
+ Control(
151
+ "1.16",
152
+ Framework.CIS_AWS,
153
+ "Ensure IAM policies not attached directly to users",
154
+ "IAM policies should be attached to groups/roles, not users",
155
+ "medium",
156
+ ),
157
+ Control(
158
+ "1.17",
159
+ Framework.CIS_AWS,
160
+ "Ensure wildcard (*) not used in IAM policies",
161
+ "IAM policies should not use wildcards for resources",
162
+ "high",
163
+ ),
164
+ # S3
165
+ Control(
166
+ "2.1.1",
167
+ Framework.CIS_AWS,
168
+ "Ensure S3 bucket Block Public Access is enabled",
169
+ "All S3 buckets should have Block Public Access enabled",
170
+ "high",
171
+ ),
172
+ Control(
173
+ "2.1.2",
174
+ Framework.CIS_AWS,
175
+ "Ensure S3 bucket Block Public Access at account level",
176
+ "Account-level S3 Block Public Access should be enabled",
177
+ "high",
178
+ ),
179
+ Control(
180
+ "2.1.5",
181
+ Framework.CIS_AWS,
182
+ "Ensure S3 bucket access logging is enabled",
183
+ "S3 buckets should have access logging enabled",
184
+ "medium",
185
+ ),
186
+ # EC2/VPC
187
+ Control(
188
+ "5.1",
189
+ Framework.CIS_AWS,
190
+ "Ensure no open Security Groups to 0.0.0.0/0",
191
+ "Security groups should not allow 0.0.0.0/0 ingress",
192
+ "high",
193
+ ),
194
+ Control(
195
+ "5.2",
196
+ Framework.CIS_AWS,
197
+ "Ensure default security group restricts all traffic",
198
+ "VPC default security groups should not allow any traffic",
199
+ "medium",
200
+ ),
201
+ Control(
202
+ "5.3",
203
+ Framework.CIS_AWS,
204
+ "Ensure VPC flow logging is enabled",
205
+ "All VPCs should have flow logging enabled",
206
+ "medium",
207
+ ),
208
+ Control(
209
+ "5.4",
210
+ Framework.CIS_AWS,
211
+ "Ensure EC2 instances use IMDSv2",
212
+ "EC2 instances should use Instance Metadata Service v2",
213
+ "medium",
214
+ ),
215
+ ]
216
+
217
+ # SOC 2 Type II Controls
218
+ SOC2_CONTROLS = [
219
+ Control(
220
+ "CC6.1",
221
+ Framework.SOC2,
222
+ "Logical and Physical Access Controls",
223
+ "Access to system components is controlled by access policies",
224
+ "high",
225
+ ),
226
+ Control(
227
+ "CC6.2",
228
+ Framework.SOC2,
229
+ "Prior to Access",
230
+ "Users are authenticated before access is granted",
231
+ "high",
232
+ ),
233
+ Control(
234
+ "CC6.3",
235
+ Framework.SOC2,
236
+ "Role-Based Access",
237
+ "Access is based on job function and least privilege",
238
+ "high",
239
+ ),
240
+ Control(
241
+ "CC6.6",
242
+ Framework.SOC2,
243
+ "Encryption of Data",
244
+ "Data at rest and in transit is encrypted",
245
+ "high",
246
+ ),
247
+ Control(
248
+ "CC6.7",
249
+ Framework.SOC2,
250
+ "Data Disposal",
251
+ "Data is disposed of securely when no longer needed",
252
+ "medium",
253
+ ),
254
+ Control(
255
+ "CC7.1",
256
+ Framework.SOC2,
257
+ "Security Monitoring",
258
+ "Security events are detected and responded to",
259
+ "high",
260
+ ),
261
+ Control(
262
+ "CC7.2",
263
+ Framework.SOC2,
264
+ "Incident Response",
265
+ "Security incidents are managed and resolved",
266
+ "high",
267
+ ),
268
+ ]
269
+
270
+ # Mapping from finding types to controls
271
+ FINDING_TO_CONTROLS: dict[str, list[str]] = {
272
+ # IAM findings
273
+ "iam_overly_permissive_trust": ["CIS-AWS:1.17", "SOC2:CC6.3"],
274
+ "iam_wildcard_policy": ["CIS-AWS:1.17", "SOC2:CC6.3"],
275
+ "iam_unused_credentials": ["CIS-AWS:1.12", "SOC2:CC6.1"],
276
+ "iam_user_direct_policy": ["CIS-AWS:1.16", "SOC2:CC6.3"],
277
+ "iam_no_mfa": ["CIS-AWS:1.10", "SOC2:CC6.2"],
278
+ # S3 findings
279
+ "s3_public_bucket": ["CIS-AWS:2.1.1", "CIS-AWS:2.1.2", "SOC2:CC6.1"],
280
+ "s3-bucket-no-public-access-block": ["CIS-AWS:2.1.1", "CIS-AWS:2.1.2", "SOC2:CC6.1"],
281
+ "s3-bucket-public-access-block": ["CIS-AWS:2.1.1", "CIS-AWS:2.1.2", "SOC2:CC6.1"],
282
+ "s3-bucket-partial-public-access-block": ["CIS-AWS:2.1.1", "CIS-AWS:2.1.5", "SOC2:CC6.1"],
283
+ "s3-bucket-public-acl": ["CIS-AWS:2.1.1", "SOC2:CC6.1"],
284
+ "s3-bucket-authenticated-users-acl": ["CIS-AWS:2.1.1", "SOC2:CC6.1"],
285
+ "s3_no_encryption": ["SOC2:CC6.6"],
286
+ "s3_no_logging": ["CIS-AWS:2.1.5", "SOC2:CC7.1"],
287
+ # EC2/Network findings
288
+ "security_group_open_to_world": ["CIS-AWS:5.1", "SOC2:CC6.1"],
289
+ "security-group-open-to-world": ["CIS-AWS:5.1", "CIS-AWS:5.2", "SOC2:CC6.1"],
290
+ "ec2-public-ip": ["CIS-AWS:5.1", "CIS-AWS:5.2", "SOC2:CC6.1"],
291
+ "vpc_default_sg_in_use": ["CIS-AWS:5.2", "SOC2:CC6.1"],
292
+ "vpc_no_flow_logs": ["CIS-AWS:5.3", "SOC2:CC7.1"],
293
+ "ec2_imdsv1": ["CIS-AWS:5.4", "SOC2:CC6.1"],
294
+ "ec2-imdsv1-enabled": ["CIS-AWS:5.4", "SOC2:CC6.1"],
295
+ "iam-role-trust-any-principal": ["CIS-AWS:1.17", "SOC2:CC6.3"],
296
+ }
297
+
298
+
299
+ class ComplianceChecker:
300
+ """
301
+ Check compliance against frameworks based on scan findings.
302
+ """
303
+
304
+ def __init__(self):
305
+ self._controls_by_id: dict[str, Control] = {}
306
+ for ctrl in CIS_CONTROLS + SOC2_CONTROLS:
307
+ self._controls_by_id[ctrl.full_id] = ctrl
308
+
309
+ def check(
310
+ self,
311
+ findings: list[Finding],
312
+ assets: list[Asset],
313
+ *,
314
+ framework: Framework | None = None,
315
+ collection_errors: list[dict] | None = None,
316
+ ) -> ComplianceReport:
317
+ """
318
+ Check compliance based on findings.
319
+
320
+ Args:
321
+ findings: Security findings from scan
322
+ assets: Assets from scan
323
+ framework: Specific framework (default: CIS_AWS)
324
+
325
+ Returns:
326
+ ComplianceReport with pass/fail status per control
327
+ """
328
+ framework = framework or Framework.CIS_AWS
329
+ controls = CIS_CONTROLS if framework == Framework.CIS_AWS else SOC2_CONTROLS
330
+
331
+ # Build mapping: control_id -> findings that violate it
332
+ violations: dict[str, list[Finding]] = {}
333
+ for finding in findings:
334
+ control_ids = FINDING_TO_CONTROLS.get(finding.finding_type, [])
335
+ for ctrl_id in control_ids:
336
+ if ctrl_id not in violations:
337
+ violations[ctrl_id] = []
338
+ violations[ctrl_id].append(finding)
339
+
340
+ asset_types = {a.asset_type for a in assets}
341
+ error_services = {
342
+ err.get("service")
343
+ for err in (collection_errors or [])
344
+ if err.get("service")
345
+ }
346
+
347
+ # Build results
348
+ results = []
349
+ data_gaps: dict[str, dict] = {}
350
+ for ctrl in controls:
351
+ violating_findings = violations.get(ctrl.full_id, [])
352
+ required_assets = CONTROL_ASSET_REQUIREMENTS.get(ctrl.full_id, [])
353
+
354
+ if violating_findings:
355
+ status = "fail"
356
+ else:
357
+ has_assets = not required_assets or any(
358
+ asset_type in asset_types for asset_type in required_assets
359
+ )
360
+ if has_assets:
361
+ status = "pass"
362
+ else:
363
+ status = "unknown"
364
+ data_gaps[ctrl.full_id] = {
365
+ "reason": "missing_assets",
366
+ "required_assets": required_assets,
367
+ }
368
+
369
+ if status != "fail" and required_assets and error_services:
370
+ impacted = self._assets_impacted_by_errors(required_assets, error_services)
371
+ if impacted:
372
+ status = "unknown"
373
+ data_gaps[ctrl.full_id] = {
374
+ "reason": "collection_error",
375
+ "required_assets": required_assets,
376
+ "services": sorted(impacted),
377
+ }
378
+
379
+ results.append(
380
+ ComplianceResult(
381
+ control=ctrl,
382
+ status=status,
383
+ findings=violating_findings,
384
+ assets_affected=len(set(f.asset_id for f in violating_findings)),
385
+ )
386
+ )
387
+
388
+ return ComplianceReport(
389
+ framework=framework,
390
+ results=results,
391
+ data_gaps=data_gaps,
392
+ )
393
+
394
+ @staticmethod
395
+ def _assets_impacted_by_errors(
396
+ required_assets: list[str],
397
+ error_services: set[str],
398
+ ) -> set[str]:
399
+ """Map collection errors to affected control services."""
400
+ impacted: set[str] = set()
401
+ for service in error_services:
402
+ if service == "iam" and any(a.startswith("iam:") for a in required_assets):
403
+ impacted.add(service)
404
+ if service == "s3" and any(a.startswith("s3:") for a in required_assets):
405
+ impacted.add(service)
406
+ if service in {"ec2", "network"} and any(a.startswith("ec2:") for a in required_assets):
407
+ impacted.add(service)
408
+ if service == "lambda" and any(a.startswith("lambda:") for a in required_assets):
409
+ impacted.add(service)
410
+ if service == "rds" and any(a.startswith("rds:") for a in required_assets):
411
+ impacted.add(service)
412
+ return impacted
413
+
414
+ def get_control(self, control_id: str) -> Control | None:
415
+ """Get a control by ID."""
416
+ return self._controls_by_id.get(control_id)
417
+
418
+ def summary(self, report: ComplianceReport) -> dict:
419
+ """Generate summary statistics for a report."""
420
+ by_severity = {"critical": 0, "high": 0, "medium": 0, "low": 0}
421
+ failing_by_severity = {"critical": 0, "high": 0, "medium": 0, "low": 0}
422
+
423
+ for result in report.results:
424
+ sev = result.control.severity
425
+ by_severity[sev] = by_severity.get(sev, 0) + 1
426
+ if not result.is_passing:
427
+ failing_by_severity[sev] = failing_by_severity.get(sev, 0) + 1
428
+
429
+ return {
430
+ "framework": report.framework.value,
431
+ "total_controls": len(report.results),
432
+ "passing": report.passing,
433
+ "failing": report.failing,
434
+ "compliance_score": report.compliance_score,
435
+ "by_severity": by_severity,
436
+ "failing_by_severity": failing_by_severity,
437
+ }