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.
- cyntrisec/__init__.py +3 -0
- cyntrisec/__main__.py +6 -0
- cyntrisec/aws/__init__.py +6 -0
- cyntrisec/aws/collectors/__init__.py +17 -0
- cyntrisec/aws/collectors/ec2.py +30 -0
- cyntrisec/aws/collectors/iam.py +116 -0
- cyntrisec/aws/collectors/lambda_.py +45 -0
- cyntrisec/aws/collectors/network.py +70 -0
- cyntrisec/aws/collectors/rds.py +38 -0
- cyntrisec/aws/collectors/s3.py +68 -0
- cyntrisec/aws/collectors/usage.py +188 -0
- cyntrisec/aws/credentials.py +153 -0
- cyntrisec/aws/normalizers/__init__.py +17 -0
- cyntrisec/aws/normalizers/ec2.py +115 -0
- cyntrisec/aws/normalizers/iam.py +182 -0
- cyntrisec/aws/normalizers/lambda_.py +83 -0
- cyntrisec/aws/normalizers/network.py +225 -0
- cyntrisec/aws/normalizers/rds.py +130 -0
- cyntrisec/aws/normalizers/s3.py +184 -0
- cyntrisec/aws/relationship_builder.py +1359 -0
- cyntrisec/aws/scanner.py +303 -0
- cyntrisec/cli/__init__.py +5 -0
- cyntrisec/cli/analyze.py +747 -0
- cyntrisec/cli/ask.py +412 -0
- cyntrisec/cli/can.py +307 -0
- cyntrisec/cli/comply.py +226 -0
- cyntrisec/cli/cuts.py +231 -0
- cyntrisec/cli/diff.py +332 -0
- cyntrisec/cli/errors.py +105 -0
- cyntrisec/cli/explain.py +348 -0
- cyntrisec/cli/main.py +114 -0
- cyntrisec/cli/manifest.py +893 -0
- cyntrisec/cli/output.py +117 -0
- cyntrisec/cli/remediate.py +643 -0
- cyntrisec/cli/report.py +462 -0
- cyntrisec/cli/scan.py +207 -0
- cyntrisec/cli/schemas.py +391 -0
- cyntrisec/cli/serve.py +164 -0
- cyntrisec/cli/setup.py +260 -0
- cyntrisec/cli/validate.py +101 -0
- cyntrisec/cli/waste.py +323 -0
- cyntrisec/core/__init__.py +31 -0
- cyntrisec/core/business_config.py +110 -0
- cyntrisec/core/business_logic.py +131 -0
- cyntrisec/core/compliance.py +437 -0
- cyntrisec/core/cost_estimator.py +301 -0
- cyntrisec/core/cuts.py +360 -0
- cyntrisec/core/diff.py +361 -0
- cyntrisec/core/graph.py +202 -0
- cyntrisec/core/paths.py +830 -0
- cyntrisec/core/schema.py +317 -0
- cyntrisec/core/simulator.py +371 -0
- cyntrisec/core/waste.py +309 -0
- cyntrisec/mcp/__init__.py +5 -0
- cyntrisec/mcp/server.py +862 -0
- cyntrisec/storage/__init__.py +7 -0
- cyntrisec/storage/filesystem.py +344 -0
- cyntrisec/storage/memory.py +113 -0
- cyntrisec/storage/protocol.py +92 -0
- cyntrisec-0.1.7.dist-info/METADATA +672 -0
- cyntrisec-0.1.7.dist-info/RECORD +65 -0
- cyntrisec-0.1.7.dist-info/WHEEL +4 -0
- cyntrisec-0.1.7.dist-info/entry_points.txt +2 -0
- cyntrisec-0.1.7.dist-info/licenses/LICENSE +190 -0
- cyntrisec-0.1.7.dist-info/licenses/NOTICE +5 -0
cyntrisec/cli/explain.py
ADDED
|
@@ -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()
|