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,348 @@
1
+ """
2
+ explain command - Natural language explanations of findings and attack paths.
3
+
4
+ Provides agent-friendly explanations for findings, attack vectors, and compliance controls.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ import typer
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+
15
+ from cyntrisec.cli.errors import EXIT_CODE_MAP, CyntriError, ErrorCode, handle_errors
16
+ from cyntrisec.cli.output import emit_agent_or_json, resolve_format, suggested_actions
17
+ from cyntrisec.cli.schemas import ExplainResponse
18
+ from cyntrisec.core.compliance import CIS_CONTROLS, SOC2_CONTROLS
19
+
20
+ console = Console()
21
+ log = logging.getLogger(__name__)
22
+
23
+ # Finding type explanations
24
+ FINDING_EXPLANATIONS = {
25
+ "security_group_open_to_world": {
26
+ "title": "Security Group Open to World",
27
+ "severity": "HIGH",
28
+ "what": "A security group has an inbound rule allowing traffic from 0.0.0.0/0 (all IPs).",
29
+ "why": "This exposes the resource to the entire internet. Attackers can scan and probe the exposed ports, potentially leading to exploitation if vulnerabilities exist.",
30
+ "fix": "Restrict the source IP to specific trusted ranges. Use VPN or bastion hosts for remote access instead of direct internet exposure.",
31
+ "next_command": "cyntrisec cuts",
32
+ },
33
+ "security-group-open-to-world": {
34
+ "title": "Security Group Open to World",
35
+ "severity": "HIGH",
36
+ "what": "A security group has an inbound rule allowing traffic from 0.0.0.0/0 (all IPs).",
37
+ "why": "This exposes the resource to the entire internet. Attackers can scan and probe the exposed ports, potentially leading to exploitation if vulnerabilities exist.",
38
+ "fix": "Restrict the source IP to specific trusted ranges. Use VPN or bastion hosts for remote access instead of direct internet exposure.",
39
+ "next_command": "cyntrisec cuts",
40
+ },
41
+ "s3_public_bucket": {
42
+ "title": "S3 Bucket Publicly Accessible",
43
+ "severity": "CRITICAL",
44
+ "what": "An S3 bucket has public access enabled through ACLs or bucket policies.",
45
+ "why": "Public buckets can be discovered and accessed by anyone. This is a leading cause of data breaches, exposing sensitive customer data, credentials, or intellectual property.",
46
+ "fix": "Enable S3 Block Public Access at both bucket and account level. Review and remove any 'Principal: *' statements from bucket policies.",
47
+ "next_command": "cyntrisec comply --framework cis-aws",
48
+ },
49
+ "s3-bucket-partial-public-access-block": {
50
+ "title": "S3 Bucket Missing Public Access Block",
51
+ "severity": "HIGH",
52
+ "what": "An S3 bucket does not have all public access block settings enabled.",
53
+ "why": "Partial public access blocks leave gaps that could allow unintended public access to bucket contents through ACLs or bucket policies.",
54
+ "fix": "Enable all four S3 Block Public Access settings: BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy, RestrictPublicBuckets.",
55
+ "next_command": "cyntrisec comply --framework cis-aws",
56
+ },
57
+ "s3-bucket-public-access-block": {
58
+ "title": "S3 Bucket Public Access Block Disabled",
59
+ "severity": "HIGH",
60
+ "what": "An S3 bucket has public access block settings disabled.",
61
+ "why": "Without public access blocks, the bucket may be exposed to the internet through ACLs or bucket policies.",
62
+ "fix": "Enable S3 Block Public Access at both bucket and account level.",
63
+ "next_command": "cyntrisec comply --framework cis-aws",
64
+ },
65
+ "ec2-public-ip": {
66
+ "title": "EC2 Instance with Public IP",
67
+ "severity": "MEDIUM",
68
+ "what": "An EC2 instance has a public IP address assigned.",
69
+ "why": "Public IPs expose instances directly to the internet, increasing attack surface. Attackers can scan and probe exposed services.",
70
+ "fix": "Use private subnets with NAT gateways for outbound access. Use load balancers or bastion hosts for inbound access.",
71
+ "next_command": "cyntrisec analyze paths",
72
+ },
73
+ "iam_overly_permissive_trust": {
74
+ "title": "Overly Permissive IAM Trust Policy",
75
+ "severity": "HIGH",
76
+ "what": "An IAM role has a trust policy that allows assumption from broad principals (e.g., all AWS accounts, or Principal: '*').",
77
+ "why": "This could allow unauthorized cross-account access or privilege escalation if an attacker compromises any trusted entity.",
78
+ "fix": "Restrict trust policies to specific, known AWS account IDs and principals. Add conditions like external ID or source IP restrictions.",
79
+ "next_command": "cyntrisec can <role> access <target>",
80
+ },
81
+ }
82
+
83
+ # Attack vector explanations
84
+ ATTACK_VECTOR_EXPLANATIONS = {
85
+ "instance-compromise": {
86
+ "title": "Instance Compromise Attack Path",
87
+ "description": "An attacker who gains access to an EC2 instance can leverage its IAM role to access other resources.",
88
+ "stages": [
89
+ "1. **Initial Access**: Attacker exploits vulnerability or uses stolen credentials to access EC2 instance",
90
+ "2. **Credential Theft**: Instance metadata service (IMDS) provides temporary IAM credentials",
91
+ "3. **Lateral Movement**: Attacker uses IAM role permissions to access S3, RDS, or other services",
92
+ "4. **Impact**: Data exfiltration, privilege escalation, or further infrastructure compromise",
93
+ ],
94
+ "mitigations": [
95
+ "Use IMDSv2 instead of IMDSv1 to prevent SSRF-based credential theft",
96
+ "Apply least-privilege to instance IAM roles",
97
+ "Use VPC endpoints to restrict network paths",
98
+ "Enable GuardDuty for anomaly detection",
99
+ ],
100
+ },
101
+ "lateral-movement": {
102
+ "title": "Lateral Movement Attack Path",
103
+ "description": "An attacker moves from one compromised resource to another to expand their access.",
104
+ "stages": [
105
+ "1. **Initial Foothold**: Attacker compromises one resource (EC2, Lambda, etc.)",
106
+ "2. **Discovery**: Attacker enumerates accessible resources using IAM permissions",
107
+ "3. **Pivot**: Attacker uses shared credentials or trust relationships to reach new resources",
108
+ "4. **Escalation**: Each hop potentially grants access to more sensitive data or controls",
109
+ ],
110
+ "mitigations": [
111
+ "Segment networks using VPCs and security groups",
112
+ "Implement strict IAM policies with least-privilege",
113
+ "Use AWS PrivateLink for sensitive service access",
114
+ "Monitor for unusual API calls with CloudTrail",
115
+ ],
116
+ },
117
+ }
118
+
119
+
120
+ @handle_errors
121
+ def explain_cmd(
122
+ category: str = typer.Argument(
123
+ ...,
124
+ help="Category to explain: finding, path, control",
125
+ ),
126
+ identifier: str = typer.Argument(
127
+ ...,
128
+ help="Identifier of the item to explain",
129
+ ),
130
+ format: str | None = typer.Option(
131
+ None,
132
+ "--format",
133
+ "-f",
134
+ help="Output format: text, json, markdown, agent (defaults to json when piped)",
135
+ ),
136
+ ):
137
+ """
138
+ Get natural language explanations of security findings, attack paths, or compliance controls.
139
+
140
+ Examples:
141
+ cyntrisec explain finding security_group_open_to_world
142
+ cyntrisec explain path instance-compromise
143
+ cyntrisec explain control CIS-AWS:5.1
144
+ """
145
+ output_format = resolve_format(
146
+ format,
147
+ default_tty="text",
148
+ allowed=["text", "json", "markdown", "agent"],
149
+ )
150
+
151
+ if category == "finding":
152
+ _explain_finding(identifier, output_format)
153
+ elif category == "path":
154
+ _explain_path(identifier, output_format)
155
+ elif category == "control":
156
+ _explain_control(identifier, output_format)
157
+ else:
158
+ raise CyntriError(
159
+ error_code=ErrorCode.INVALID_QUERY,
160
+ message=f"Unknown category '{category}'. Use: finding, path, control",
161
+ exit_code=EXIT_CODE_MAP["usage"],
162
+ )
163
+
164
+
165
+ def _explain_finding(finding_type: str, format: str):
166
+ """Explain a finding type."""
167
+ explanation = FINDING_EXPLANATIONS.get(finding_type)
168
+
169
+ if not explanation:
170
+ # Try to find partial match
171
+ for key, exp in FINDING_EXPLANATIONS.items():
172
+ if finding_type in key:
173
+ explanation = exp
174
+ break
175
+
176
+ if not explanation:
177
+ raise CyntriError(
178
+ error_code=ErrorCode.INVALID_QUERY,
179
+ message=f"No explanation found for finding type '{finding_type}'",
180
+ exit_code=EXIT_CODE_MAP["usage"],
181
+ )
182
+
183
+ if format in {"json", "agent"}:
184
+ next_cmd = explanation.get("next_command")
185
+ actions = suggested_actions([(next_cmd, "Suggested next step")] if next_cmd else [])
186
+ emit_agent_or_json(
187
+ format,
188
+ {"type": "finding", "id": finding_type, "explanation": explanation},
189
+ suggested=actions,
190
+ schema=ExplainResponse,
191
+ )
192
+ return
193
+
194
+ if format == "markdown":
195
+ md = (
196
+ f"# {explanation['title']}\n\n"
197
+ f"**Severity:** {explanation['severity']}\n\n"
198
+ f"## What is it?\n{explanation['what']}\n\n"
199
+ f"## Why does it matter?\n{explanation['why']}\n\n"
200
+ f"## How to fix it?\n{explanation['fix']}\n"
201
+ )
202
+ # Output raw markdown text, not Rich-rendered
203
+ typer.echo(md)
204
+ return
205
+
206
+ _render_finding_explanation(explanation)
207
+
208
+
209
+ def _explain_path(attack_vector: str, format: str):
210
+ """Explain an attack path/vector."""
211
+ explanation = ATTACK_VECTOR_EXPLANATIONS.get(attack_vector)
212
+
213
+ if not explanation:
214
+ for key, exp in ATTACK_VECTOR_EXPLANATIONS.items():
215
+ if attack_vector in key:
216
+ explanation = exp
217
+ break
218
+
219
+ if not explanation:
220
+ raise CyntriError(
221
+ error_code=ErrorCode.INVALID_QUERY,
222
+ message=f"No explanation found for attack vector '{attack_vector}'",
223
+ exit_code=EXIT_CODE_MAP["usage"],
224
+ )
225
+
226
+ if format in {"json", "agent"}:
227
+ emit_agent_or_json(
228
+ format,
229
+ {"type": "path", "id": attack_vector, "explanation": explanation},
230
+ suggested=suggested_actions(
231
+ [
232
+ ("cyntrisec analyze paths --format agent", "List concrete paths of this type"),
233
+ ]
234
+ ),
235
+ schema=ExplainResponse,
236
+ )
237
+ return
238
+
239
+ if format == "markdown":
240
+ md = (
241
+ f"# {explanation['title']}\n\n"
242
+ f"{explanation['description']}\n\n"
243
+ f"## Attack Stages\n" + "\n".join(explanation["stages"]) + "\n\n"
244
+ "## Mitigations\n" + "\n".join(f"- {m}" for m in explanation["mitigations"])
245
+ )
246
+ # Output raw markdown text, not Rich-rendered
247
+ typer.echo(md)
248
+ return
249
+
250
+ _render_path_explanation(explanation)
251
+
252
+
253
+ def _explain_control(control_id: str, format: str):
254
+ """Explain a compliance control."""
255
+ all_controls = CIS_CONTROLS + SOC2_CONTROLS
256
+
257
+ control = None
258
+ for c in all_controls:
259
+ if c.id == control_id or c.full_id == control_id:
260
+ control = c
261
+ break
262
+
263
+ if not control:
264
+ raise CyntriError(
265
+ error_code=ErrorCode.INVALID_QUERY,
266
+ message=f"No control found for '{control_id}'",
267
+ exit_code=EXIT_CODE_MAP["usage"],
268
+ )
269
+
270
+ explanation = {
271
+ "id": control.full_id,
272
+ "title": control.title,
273
+ "description": control.description,
274
+ "severity": control.severity,
275
+ "framework": control.framework.value,
276
+ }
277
+
278
+ if format in {"json", "agent"}:
279
+ emit_agent_or_json(
280
+ format,
281
+ {"type": "control", "id": control.full_id, "explanation": explanation},
282
+ suggested=suggested_actions(
283
+ [
284
+ ("cyntrisec comply --format agent", "Run a full compliance check"),
285
+ ]
286
+ ),
287
+ schema=ExplainResponse,
288
+ )
289
+ elif format == "markdown":
290
+ md = (
291
+ f"# {control.full_id}: {control.title}\n\n"
292
+ f"{control.description}\n\n"
293
+ f"**Severity:** {control.severity.upper()}\n"
294
+ )
295
+ # Output raw markdown text, not Rich-rendered
296
+ typer.echo(md)
297
+ else:
298
+ console.print()
299
+ console.print(
300
+ Panel(
301
+ f"{control.title}\n\n{control.description}\n\nSeverity: {control.severity.upper()}",
302
+ title=f"Control {control.full_id}",
303
+ border_style="cyan",
304
+ )
305
+ )
306
+
307
+
308
+ def _render_finding_explanation(exp: dict):
309
+ """Render a finding explanation as rich text."""
310
+ console.print()
311
+
312
+ sev_color = {"CRITICAL": "red", "HIGH": "red", "MEDIUM": "yellow", "LOW": "dim"}.get(
313
+ exp["severity"], "white"
314
+ )
315
+
316
+ console.print(
317
+ Panel(
318
+ f"{exp['title']}\n\n"
319
+ f"Severity: {exp['severity']}\n\n"
320
+ f"What is it?\n{exp['what']}\n\n"
321
+ f"Why does it matter?\n{exp['why']}\n\n"
322
+ f"How to fix it?\n{exp['fix']}",
323
+ title="Finding Explanation",
324
+ border_style=sev_color,
325
+ )
326
+ )
327
+
328
+ if exp.get("next_command"):
329
+ console.print(f"\n[cyan]Suggested next command:[/cyan] `{exp['next_command']}`")
330
+
331
+
332
+ def _render_path_explanation(exp: dict):
333
+ """Render an attack path explanation as rich text."""
334
+ console.print()
335
+
336
+ stages = "\n".join(exp["stages"])
337
+ mitigations = "\n".join(f"- {m}" for m in exp["mitigations"])
338
+
339
+ console.print(
340
+ Panel(
341
+ f"**{exp['title']}**\n\n"
342
+ f"{exp['description']}\n\n"
343
+ f"**Attack Stages:**\n{stages}\n\n"
344
+ f"**Mitigations:**\n{mitigations}",
345
+ title="Attack Path Explanation",
346
+ border_style="red",
347
+ )
348
+ )
cyntrisec/cli/main.py ADDED
@@ -0,0 +1,114 @@
1
+ """
2
+ Cyntrisec CLI
3
+
4
+ Main entry point for the CLI application.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import sys
11
+
12
+ import typer
13
+
14
+ # Create main app
15
+ app = typer.Typer(
16
+ name="cyntrisec",
17
+ help="AWS capability graph analysis and attack path discovery",
18
+ no_args_is_help=True,
19
+ add_completion=False,
20
+ )
21
+
22
+
23
+ @app.callback()
24
+ def main(
25
+ verbose: bool = typer.Option(
26
+ False,
27
+ "--verbose",
28
+ "-v",
29
+ help="Enable verbose logging",
30
+ ),
31
+ quiet: bool = typer.Option(
32
+ False,
33
+ "--quiet",
34
+ "-q",
35
+ help="Suppress all output except errors",
36
+ ),
37
+ ):
38
+ """
39
+ Cyntrisec - AWS Capability Graph Analysis
40
+
41
+ A read-only CLI tool for:
42
+
43
+ - AWS attack path discovery
44
+
45
+ - Security posture analysis
46
+
47
+ - Cost optimization opportunities
48
+
49
+ Run 'cyntrisec COMMAND --help' for command-specific help.
50
+ """
51
+ # Configure logging
52
+ if quiet:
53
+ level = logging.ERROR
54
+ elif verbose:
55
+ level = logging.DEBUG
56
+ else:
57
+ level = logging.INFO
58
+
59
+ logging.basicConfig(
60
+ level=level,
61
+ format="%(message)s" if not verbose else "%(asctime)s %(levelname)s %(name)s: %(message)s",
62
+ stream=sys.stderr,
63
+ )
64
+
65
+
66
+ @app.command()
67
+ def version():
68
+ """Show version information."""
69
+ from cyntrisec import __version__
70
+
71
+ typer.echo(f"cyntrisec {__version__}")
72
+
73
+
74
+ # Register subcommands at module load time
75
+ # Import inside try/except for graceful handling if deps missing
76
+ try:
77
+ from cyntrisec.cli.analyze import analyze_app
78
+ from cyntrisec.cli.ask import ask_cmd
79
+ from cyntrisec.cli.can import can_cmd
80
+ from cyntrisec.cli.comply import comply_cmd
81
+ from cyntrisec.cli.cuts import cuts_cmd
82
+ from cyntrisec.cli.diff import diff_cmd
83
+ from cyntrisec.cli.explain import explain_cmd
84
+ from cyntrisec.cli.manifest import manifest_cmd
85
+ from cyntrisec.cli.remediate import remediate_cmd
86
+ from cyntrisec.cli.report import report_cmd
87
+ from cyntrisec.cli.scan import scan_cmd
88
+ from cyntrisec.cli.serve import serve_cmd
89
+ from cyntrisec.cli.setup import setup_app
90
+ from cyntrisec.cli.validate import validate_role_cmd
91
+ from cyntrisec.cli.waste import waste_cmd
92
+
93
+ app.command("scan")(scan_cmd)
94
+ app.add_typer(analyze_app, name="analyze", help="Analyze scan results")
95
+ app.command("report")(report_cmd)
96
+ app.add_typer(setup_app, name="setup", help="Setup commands")
97
+ app.command("validate-role")(validate_role_cmd)
98
+ app.command("cuts")(cuts_cmd)
99
+ app.command("waste")(waste_cmd)
100
+ app.command("can")(can_cmd)
101
+ app.command("diff")(diff_cmd)
102
+ app.command("comply")(comply_cmd)
103
+ app.command("manifest")(manifest_cmd)
104
+ app.command("explain")(explain_cmd)
105
+ app.command("serve")(serve_cmd)
106
+ app.command("remediate")(remediate_cmd)
107
+ app.command("ask")(ask_cmd)
108
+ except ImportError:
109
+ # Allow --version and --help to work even if deps missing
110
+ pass
111
+
112
+
113
+ if __name__ == "__main__":
114
+ app()