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/schemas.py
ADDED
|
@@ -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
|