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,391 @@
1
+ """
2
+ Response schemas for CLI commands.
3
+
4
+ These Pydantic models provide lightweight enforcement for JSON/agent
5
+ outputs so agents can rely on a stable contract. Each command can
6
+ reference a schema by name in emit_agent_or_json to validate data
7
+ before it is printed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from pydantic import BaseModel, ConfigDict, Field
15
+
16
+
17
+ class BaseSchema(BaseModel):
18
+ """Base config shared by response schemas."""
19
+
20
+ model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
21
+
22
+
23
+ class ActionModel(BaseSchema):
24
+ command: str
25
+ reason: str
26
+
27
+
28
+ class ArtifactPathsModel(BaseSchema):
29
+ snapshot_dir: str | None = None
30
+ snapshot: str | None = None
31
+ assets: str | None = None
32
+ relationships: str | None = None
33
+ attack_paths: str | None = None
34
+ findings: str | None = None
35
+
36
+
37
+ class AgentEnvelope(BaseSchema):
38
+ schema_version: str
39
+ status: str
40
+ data: Any
41
+ message: str | None = None
42
+ error_code: str | None = None
43
+ artifact_paths: ArtifactPathsModel | None = None
44
+ suggested_actions: list[ActionModel] | None = None
45
+
46
+
47
+ class ScanResponse(BaseSchema):
48
+ scan_id: str
49
+ snapshot_id: str
50
+ status: str
51
+ account_id: str | None = None
52
+ regions: list[str]
53
+ asset_count: int
54
+ relationship_count: int
55
+ finding_count: int
56
+ attack_path_count: int
57
+ warnings: list[str] | None = None
58
+
59
+
60
+ class AttackPathOut(BaseSchema):
61
+ id: str
62
+ snapshot_id: str | None = None
63
+ source_asset_id: str
64
+ target_asset_id: str
65
+ path_asset_ids: list[str]
66
+ path_relationship_ids: list[str]
67
+ attack_vector: str
68
+ path_length: int
69
+ entry_confidence: float
70
+ exploitability_score: float
71
+ impact_score: float
72
+ risk_score: float
73
+ confidence_level: str | None = None
74
+ confidence_reason: str | None = None
75
+ attack_chain_relationship_ids: list[str] | None = None
76
+ context_relationship_ids: list[str] | None = None
77
+ proof: dict[str, Any] = Field(default_factory=dict)
78
+
79
+ model_config = ConfigDict(extra="allow")
80
+
81
+
82
+ class AnalyzePathsResponse(BaseSchema):
83
+ paths: list[AttackPathOut]
84
+ returned: int
85
+ total: int
86
+
87
+
88
+ class FindingOut(BaseSchema):
89
+ id: str | None = None
90
+ snapshot_id: str | None = None
91
+ asset_id: str | None = None
92
+ finding_type: str
93
+ severity: str
94
+ title: str
95
+ description: str | None = None
96
+ remediation: str | None = None
97
+ evidence: dict[str, Any] = Field(default_factory=dict)
98
+
99
+ model_config = ConfigDict(extra="allow")
100
+
101
+
102
+ class AnalyzeFindingsResponse(BaseSchema):
103
+ findings: list[FindingOut]
104
+ total: int
105
+ filter: str
106
+
107
+
108
+ class AnalyzeStatsResponse(BaseSchema):
109
+ snapshot_id: str | None = None
110
+ scan_id: str | None = None
111
+ account_id: str | None = None
112
+ asset_count: int
113
+ relationship_count: int
114
+ finding_count: int
115
+ path_count: int
116
+ regions: list[str]
117
+ status: str
118
+
119
+
120
+ class WasteCandidate(BaseSchema):
121
+ name: str
122
+ asset_type: str
123
+ reason: str
124
+ asset_id: str | None = None
125
+ monthly_cost_usd: float | None = None
126
+
127
+
128
+ class BusinessAsset(BaseSchema):
129
+ name: str
130
+ asset_type: str
131
+ reason: str
132
+ asset_id: str | None = None
133
+ tags: dict[str, str] = Field(default_factory=dict)
134
+
135
+
136
+ class BusinessAnalysisResponse(BaseSchema):
137
+ entrypoints_requested: list[str]
138
+ entrypoints_found: list[str]
139
+ attackable_count: int
140
+ business_required_count: int
141
+ waste_candidate_count: int
142
+ waste_candidates: list[WasteCandidate]
143
+ business_assets: list[BusinessAsset] | None = None
144
+ unknown_assets: list[BusinessAsset] | None = None
145
+
146
+
147
+ class CutRemediation(BaseSchema):
148
+ priority: int
149
+ action: str
150
+ description: str
151
+ relationship_type: str | None = None
152
+ source: str | None = None
153
+ target: str | None = None
154
+ paths_blocked: int
155
+ path_ids: list[str] = Field(default_factory=list)
156
+ # Cost fields
157
+ estimated_monthly_savings: float | None = None
158
+ cost_source: str | None = None
159
+ cost_confidence: str | None = None
160
+ cost_assumptions: list[str] | None = None
161
+
162
+
163
+ class CutsResponse(BaseSchema):
164
+ snapshot_id: str | None = None
165
+ account_id: str | None = None
166
+ total_paths: int
167
+ paths_blocked: int
168
+ coverage: float
169
+ remediations: list[CutRemediation]
170
+
171
+
172
+ class WasteCapability(BaseSchema):
173
+ service: str | None = None
174
+ service_name: str | None = None
175
+ days_unused: int | None = None
176
+ risk_level: str
177
+ recommendation: str
178
+ # Cost estimation fields
179
+ monthly_cost_usd_estimate: float | None = None
180
+ cost_source: str | None = None
181
+ confidence: str | None = None
182
+ assumptions: list[str] | None = None
183
+
184
+
185
+ class WasteRoleReport(BaseSchema):
186
+ role_arn: str | None = None
187
+ role_name: str
188
+ total_services: int
189
+ unused_services: int
190
+ reduction: float
191
+ unused_capabilities: list[WasteCapability]
192
+
193
+
194
+ class WasteResponse(BaseSchema):
195
+ snapshot_id: str | None = None
196
+ account_id: str | None = None
197
+ days_threshold: int
198
+ total_permissions: int
199
+ total_unused: int
200
+ blast_radius_reduction: float
201
+ roles: list[WasteRoleReport]
202
+
203
+
204
+ class CanSimulation(BaseSchema):
205
+ action: str
206
+ resource: str | None = None
207
+ decision: str
208
+ matched_statements: int
209
+
210
+
211
+ class CanResponse(BaseSchema):
212
+ snapshot_id: str | None = None
213
+ principal: str
214
+ resource: str
215
+ action: str | None = None
216
+ can_access: bool
217
+ simulations: list[CanSimulation]
218
+ proof: dict[str, Any] = Field(default_factory=dict)
219
+ mode: str | None = None
220
+ disclaimer: str | None = None
221
+
222
+
223
+ class DiffChange(BaseSchema):
224
+ change_type: str
225
+ path_id: str | None = None
226
+ detail: dict[str, Any] = Field(default_factory=dict)
227
+
228
+ model_config = ConfigDict(extra="allow")
229
+
230
+
231
+ class DiffResponse(BaseSchema):
232
+ has_regressions: bool
233
+ has_improvements: bool
234
+ summary: dict[str, Any]
235
+ path_changes: list[DiffChange]
236
+ # Optional extra fields
237
+ old_snapshot: dict[str, Any] | None = None
238
+ new_snapshot: dict[str, Any] | None = None
239
+ finding_changes: list[dict[str, Any]] | None = None
240
+ asset_changes: list[dict[str, Any]] | None = None
241
+ relationship_changes: list[dict[str, Any]] | None = None
242
+
243
+ model_config = ConfigDict(extra="allow")
244
+
245
+
246
+ class ControlResult(BaseSchema):
247
+ id: str
248
+ title: str
249
+ status: str
250
+ severity: str | None = None
251
+ description: str | None = None
252
+
253
+
254
+ class DataGap(BaseSchema):
255
+ control_id: str
256
+ reason: str
257
+ required_assets: list[str] | None = None
258
+ services: list[str] | None = None
259
+
260
+
261
+ class ComplyResponse(BaseSchema):
262
+ framework: str
263
+ compliance_score: float
264
+ passing: int
265
+ failing: int
266
+ controls: list[ControlResult]
267
+ data_gaps: list[DataGap] | None = None
268
+
269
+
270
+ class ReportResponse(BaseSchema):
271
+ output_path: str
272
+ snapshot_id: str | None = None
273
+ account_id: str | None = None
274
+ findings: int
275
+ paths: int
276
+
277
+
278
+ class ManifestResponse(BaseSchema):
279
+ name: str
280
+ version: str
281
+ description: str
282
+ capabilities: list[dict[str, Any]]
283
+ schemas: dict[str, Any]
284
+ agentic_features: dict[str, Any]
285
+ usage_pattern: list[str]
286
+
287
+ model_config = ConfigDict(extra="allow")
288
+
289
+
290
+ class RemediationItem(BaseSchema):
291
+ priority: int
292
+ action: str
293
+ description: str
294
+ source: str | None = None
295
+ target: str | None = None
296
+ relationship_type: str | None = None
297
+ paths_blocked: int
298
+ terraform: str | None = None
299
+ status: str | None = None
300
+ terraform_path: str | None = None
301
+ terraform_result: dict[str, Any] | None = None
302
+
303
+
304
+ class RemediateApplyResult(BaseSchema):
305
+ mode: str
306
+ output_path: str | None = None
307
+ terraform_path: str | None = None
308
+ terraform_dir: str | None = None
309
+ plan_exit_code: int | None = None
310
+ plan_summary: str | None = None
311
+ results: list[RemediationItem] | None = None
312
+
313
+
314
+ class RemediateResponse(BaseSchema):
315
+ snapshot_id: str | None = None
316
+ account_id: str | None = None
317
+ total_paths: int
318
+ paths_blocked: int
319
+ coverage: float
320
+ plan: list[RemediationItem]
321
+ applied: bool
322
+ mode: str
323
+ output_path: str | None = None
324
+ terraform_path: str | None = None
325
+ terraform_dir: str | None = None
326
+ apply: RemediateApplyResult | None = None
327
+
328
+
329
+ class AskResponse(BaseSchema):
330
+ query: str
331
+ intent: str
332
+ results: dict[str, Any]
333
+ snapshot_id: str | None = None
334
+ entities: dict[str, Any]
335
+ resolved: str
336
+
337
+
338
+ class ExplainResponse(BaseSchema):
339
+ type: str
340
+ id: str
341
+ explanation: dict[str, Any]
342
+
343
+
344
+ class SetupIamResponse(BaseSchema):
345
+ account_id: str
346
+ role_name: str
347
+ external_id: str | None = None
348
+ template_format: str
349
+ template: str
350
+ output_path: str | None = None
351
+
352
+
353
+ class ValidateRoleResponse(BaseSchema):
354
+ success: bool
355
+ role_arn: str
356
+ account: str | None = None
357
+ arn: str | None = None
358
+ user_id: str | None = None
359
+ error: str | None = None
360
+ error_type: str | None = None
361
+
362
+
363
+ class ServeToolsResponse(BaseSchema):
364
+ tools: list[dict[str, Any]]
365
+
366
+
367
+ SCHEMA_REGISTRY = {
368
+ "scan": ScanResponse,
369
+ "analyze_paths": AnalyzePathsResponse,
370
+ "analyze_findings": AnalyzeFindingsResponse,
371
+ "analyze_stats": AnalyzeStatsResponse,
372
+ "analyze_business": BusinessAnalysisResponse,
373
+ "cuts": CutsResponse,
374
+ "waste": WasteResponse,
375
+ "can": CanResponse,
376
+ "diff": DiffResponse,
377
+ "comply": ComplyResponse,
378
+ "report": ReportResponse,
379
+ "manifest": ManifestResponse,
380
+ "remediate": RemediateResponse,
381
+ "ask": AskResponse,
382
+ "explain": ExplainResponse,
383
+ "setup_iam": SetupIamResponse,
384
+ "validate_role": ValidateRoleResponse,
385
+ "serve_tools": ServeToolsResponse,
386
+ }
387
+
388
+
389
+ def schema_json() -> dict[str, Any]:
390
+ """Return JSON schemas for manifest exposure."""
391
+ return {name: model.model_json_schema() for name, model in SCHEMA_REGISTRY.items()}
cyntrisec/cli/serve.py ADDED
@@ -0,0 +1,164 @@
1
+ """
2
+ serve command - Run Cyntrisec as an MCP server for AI agents.
3
+
4
+ Usage:
5
+ cyntrisec serve # Start MCP server (stdio)
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import sys
12
+
13
+ import typer
14
+ from rich.console import Console
15
+
16
+ from cyntrisec.cli.errors import EXIT_CODE_MAP, CyntriError, ErrorCode, handle_errors
17
+ from cyntrisec.cli.output import emit_agent_or_json, resolve_format
18
+ from cyntrisec.cli.schemas import ServeToolsResponse
19
+
20
+ console = Console()
21
+ log = logging.getLogger(__name__)
22
+
23
+
24
+ @handle_errors
25
+ def serve_cmd(
26
+ transport: str = typer.Option(
27
+ "stdio",
28
+ "--transport",
29
+ "-t",
30
+ help="Transport mode: stdio",
31
+ ),
32
+ list_tools: bool = typer.Option(
33
+ False,
34
+ "--list-tools",
35
+ "-l",
36
+ help="List available MCP tools and exit",
37
+ ),
38
+ format: str | None = typer.Option(
39
+ None,
40
+ "--format",
41
+ "-f",
42
+ help="Output format when listing tools: text, json, agent",
43
+ ),
44
+ ):
45
+ """
46
+ Run Cyntrisec as an MCP (Model Context Protocol) server.
47
+
48
+ This allows AI agents to invoke Cyntrisec tools directly
49
+ for security analysis, attack path discovery, and remediation.
50
+
51
+ Note: The server uses stdio transport and will exit if no client is connected.
52
+ Use with an MCP client (e.g., Claude Desktop, Cursor) that maintains the connection.
53
+
54
+ Tools exposed:
55
+ - list_tools: List all available tools
56
+ - set_session_snapshot: Set active snapshot for session
57
+ - get_scan_summary: Get latest scan stats
58
+ - get_attack_paths: List discovered attack paths
59
+ - get_remediations: Find optimal fixes
60
+ - check_access: Test "can X access Y"
61
+ - get_unused_permissions: Find waste
62
+ - check_compliance: Check CIS/SOC2
63
+ - compare_scans: Detect regressions
64
+ """
65
+ resolved_format = resolve_format(format, default_tty="text", allowed=["text", "json", "agent"])
66
+
67
+ if list_tools:
68
+ tools = _list_tools_data()
69
+ if resolved_format in {"json", "agent"}:
70
+ emit_agent_or_json(resolved_format, {"tools": tools}, schema=ServeToolsResponse)
71
+ else:
72
+ console.print_json(data={"tools": tools})
73
+ return
74
+
75
+ try:
76
+ from cyntrisec.mcp.server import HAS_MCP, run_mcp_server
77
+ except ImportError as e:
78
+ raise CyntriError(
79
+ error_code=ErrorCode.INTERNAL_ERROR,
80
+ message=f"Import error: {e}",
81
+ exit_code=EXIT_CODE_MAP["usage"],
82
+ )
83
+
84
+ if not HAS_MCP:
85
+ raise CyntriError(
86
+ error_code=ErrorCode.INVALID_QUERY,
87
+ message="MCP SDK not installed. Install with: pip install mcp",
88
+ exit_code=EXIT_CODE_MAP["usage"],
89
+ )
90
+
91
+ stderr_console = Console(file=sys.stderr)
92
+ stderr_console.print("[cyan]Starting MCP server (stdio transport)...[/cyan]")
93
+ stderr_console.print("[dim]AI agents can now invoke Cyntrisec tools[/dim]")
94
+ try:
95
+ run_mcp_server()
96
+ except Exception as e:
97
+ log.exception("Error in serve")
98
+ raise CyntriError(
99
+ error_code=ErrorCode.INTERNAL_ERROR,
100
+ message=str(e),
101
+ exit_code=EXIT_CODE_MAP["internal"],
102
+ )
103
+
104
+
105
+ def _list_tools_data():
106
+ """Return available MCP tools."""
107
+ tools = [
108
+ {
109
+ "name": "list_tools",
110
+ "description": "List all available Cyntrisec tools",
111
+ "parameters": [],
112
+ },
113
+ {
114
+ "name": "set_session_snapshot",
115
+ "description": "Set active snapshot for session",
116
+ "parameters": [{"name": "snapshot_id", "type": "string", "required": False}],
117
+ },
118
+ {
119
+ "name": "get_scan_summary",
120
+ "description": "Get summary of the latest AWS scan",
121
+ "parameters": [],
122
+ },
123
+ {
124
+ "name": "get_attack_paths",
125
+ "description": "Get discovered attack paths with risk scores",
126
+ "parameters": [{"name": "max_paths", "type": "integer", "default": 10}],
127
+ },
128
+ {
129
+ "name": "get_remediations",
130
+ "description": "Find minimal set of fixes to block attack paths",
131
+ "parameters": [{"name": "max_cuts", "type": "integer", "default": 5}],
132
+ },
133
+ {
134
+ "name": "check_access",
135
+ "description": "Test if a principal can access a resource",
136
+ "parameters": [
137
+ {"name": "principal", "type": "string", "required": True},
138
+ {"name": "resource", "type": "string", "required": True},
139
+ ],
140
+ },
141
+ {
142
+ "name": "get_unused_permissions",
143
+ "description": "Find unused IAM permissions",
144
+ "parameters": [{"name": "days_threshold", "type": "integer", "default": 90}],
145
+ },
146
+ {
147
+ "name": "check_compliance",
148
+ "description": "Check CIS AWS or SOC 2 compliance",
149
+ "parameters": [
150
+ {
151
+ "name": "framework",
152
+ "type": "string",
153
+ "enum": ["cis-aws", "soc2"],
154
+ "default": "cis-aws",
155
+ }
156
+ ],
157
+ },
158
+ {
159
+ "name": "compare_scans",
160
+ "description": "Compare latest scan to previous for regressions",
161
+ "parameters": [],
162
+ },
163
+ ]
164
+ return tools