feat: Terraform Agent, expanded OCI Explorer (40+ resources), and MCP improvements
Add Terraform Agent tab with AI-powered IaC generation (plan/apply/destroy lifecycle, workspace management, compartment selection, Terraform CLI v1.7.5 in container). Expand OCI Explorer from 6 to 40+ resource types across 8 categories with tree-view navigation and resizable panels. Add compartment filtering and error tracking to MCP CIS server, increase tool timeout to 30min with auto-retry. Add chat sidebar, chat audit logs, and tune memory compaction (6K threshold, 20 recent messages). Trim model catalog from 69 to 15 curated models. Update README for v2.1.
This commit is contained in:
@@ -25,7 +25,8 @@ server = Server("cis-compliance")
|
||||
# session_key -> {checker, sections_collected: set, sections_analyzed: set, ...}
|
||||
_sessions: dict = {}
|
||||
SESSION_TTL = 7200 # 2 hours — reduce cold starts
|
||||
TOOL_TIMEOUT = 300 # 5 min max per tool call
|
||||
TOOL_TIMEOUT = 1800 # 30 min max per tool call
|
||||
TOOL_RETRIES = 1 # 1 automatic retry on failure
|
||||
|
||||
|
||||
def _session_key(config_id: str, regions: list[str] | None = None) -> str:
|
||||
@@ -233,10 +234,15 @@ def _collect_section(session, section: str):
|
||||
if not deps:
|
||||
return
|
||||
|
||||
if "collection_errors" not in session:
|
||||
session["collection_errors"] = {}
|
||||
errors = []
|
||||
|
||||
def _run_method(fn_name):
|
||||
try:
|
||||
getattr(checker, fn_name)()
|
||||
except Exception as e:
|
||||
errors.append({"collector": fn_name, "error": str(e)[:200]})
|
||||
print(f"Warning: {fn_name} failed: {e}")
|
||||
|
||||
# Home region collectors (parallel)
|
||||
@@ -249,6 +255,9 @@ def _collect_section(session, section: str):
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=8) as ex:
|
||||
list(ex.map(_run_method, deps["regional"]))
|
||||
|
||||
if errors:
|
||||
session["collection_errors"][section] = errors
|
||||
|
||||
session["sections_collected"].add(section)
|
||||
|
||||
|
||||
@@ -312,6 +321,36 @@ def _section_summary(checker, section_key: str) -> dict:
|
||||
"passed": passed, "failed": failed, "checks": checks}
|
||||
|
||||
|
||||
def _finding_in_compartment(finding, compartment_id):
|
||||
if isinstance(finding, dict):
|
||||
fcomp = finding.get("compartment_id")
|
||||
if fcomp == compartment_id:
|
||||
return True
|
||||
if fcomp is None:
|
||||
return True # recurso sem compartment (IAM tenancy-level) → incluir
|
||||
return False
|
||||
|
||||
|
||||
def _filter_by_compartment(result, compartment_id):
|
||||
passed = failed = 0
|
||||
for check in result["checks"]:
|
||||
original = check["findings"]
|
||||
filtered = [f for f in original if _finding_in_compartment(f, compartment_id)]
|
||||
check["findings"] = filtered
|
||||
check["findings_count"] = len(filtered)
|
||||
if check["status"] == "FAIL" and len(filtered) == 0:
|
||||
check["status"] = "PASS"
|
||||
if check["status"] == "PASS":
|
||||
passed += 1
|
||||
elif check["status"] == "FAIL":
|
||||
failed += 1
|
||||
total = passed + failed
|
||||
result["passed"] = passed
|
||||
result["failed"] = failed
|
||||
result["compliance_score"] = round(passed / total * 100, 1) if total > 0 else 0
|
||||
result["compartment_filter"] = compartment_id
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Remediation Data
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
@@ -451,36 +490,42 @@ TOOLS = [
|
||||
inputSchema={"type": "object", "properties": {
|
||||
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
|
||||
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
|
||||
}, "required": ["config_id"]}),
|
||||
Tool(name="cis_scan_networking",
|
||||
description="Coleta dados de rede e executa checks CIS 2.1-2.8: security lists, NSGs, VCNs, subnets, OIC, OAC, ADB.",
|
||||
inputSchema={"type": "object", "properties": {
|
||||
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
|
||||
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
|
||||
}, "required": ["config_id"]}),
|
||||
Tool(name="cis_scan_compute",
|
||||
description="Coleta instâncias Compute e executa checks CIS 3.1-3.3: metadata v2, secure boot, in-transit encryption. Rápido.",
|
||||
inputSchema={"type": "object", "properties": {
|
||||
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
|
||||
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
|
||||
}, "required": ["config_id"]}),
|
||||
Tool(name="cis_scan_logging_monitoring",
|
||||
description="Coleta dados de logging/monitoring e executa checks CIS 4.1-4.18: tags, notificações, events, Cloud Guard, KMS, logs.",
|
||||
inputSchema={"type": "object", "properties": {
|
||||
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
|
||||
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
|
||||
}, "required": ["config_id"]}),
|
||||
Tool(name="cis_scan_storage",
|
||||
description="Coleta dados de storage e executa checks CIS 5.1.1-5.3.1: buckets, block/boot volumes, file storage, CMK, versioning.",
|
||||
inputSchema={"type": "object", "properties": {
|
||||
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
|
||||
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
|
||||
}, "required": ["config_id"]}),
|
||||
Tool(name="cis_scan_asset_management",
|
||||
description="Coleta dados de assets e executa checks CIS 6.1-6.2: compartments, recursos no root.",
|
||||
inputSchema={"type": "object", "properties": {
|
||||
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||
"regions": {"type": "array", "items": {"type": "string"}, "description": "Regiões específicas (ex: ['us-ashburn-1']). Vazio = todas."},
|
||||
"compartment_id": {"type": "string", "description": "OCID de compartment para filtrar findings. Vazio = todos os compartments."},
|
||||
}, "required": ["config_id"]}),
|
||||
Tool(name="cis_get_check",
|
||||
description="Retorna findings detalhados de um check CIS individual por ID (ex: '1.7', '2.1'). Requer scan da seção correspondente.",
|
||||
@@ -529,11 +574,18 @@ def _handle_cis_list_checks(args):
|
||||
def _handle_scan_section(args, section: str):
|
||||
config_id = args["config_id"]
|
||||
regions = args.get("regions") or None
|
||||
compartment_id = args.get("compartment_id") or None
|
||||
session = _get_or_create_checker(config_id, regions=regions)
|
||||
_analyze_section(session, section)
|
||||
result = _section_summary(session["checker"], section)
|
||||
if regions:
|
||||
result["regions_filter"] = regions
|
||||
if compartment_id:
|
||||
_filter_by_compartment(result, compartment_id)
|
||||
col_errors = session.get("collection_errors", {}).get(section, [])
|
||||
if col_errors:
|
||||
result["partial_errors"] = col_errors
|
||||
result["warning"] = f"{len(col_errors)} coletores falharam — resultado pode estar incompleto"
|
||||
return result
|
||||
|
||||
def _handle_cis_get_check(args):
|
||||
@@ -620,17 +672,22 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
||||
handler = TOOL_HANDLERS.get(name)
|
||||
if not handler:
|
||||
return [TextContent(type="text", text=json.dumps({"error": f"Tool '{name}' not found"}))]
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await asyncio.wait_for(
|
||||
loop.run_in_executor(None, partial(handler, arguments)),
|
||||
timeout=TOOL_TIMEOUT
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, default=str, ensure_ascii=False))]
|
||||
except asyncio.TimeoutError:
|
||||
return [TextContent(type="text", text=json.dumps({"error": f"Tool '{name}' timed out after {TOOL_TIMEOUT}s"}))]
|
||||
except Exception as e:
|
||||
return [TextContent(type="text", text=json.dumps({"error": str(e), "traceback": traceback.format_exc()}))]
|
||||
last_error = None
|
||||
loop = asyncio.get_event_loop()
|
||||
for attempt in range(1 + TOOL_RETRIES):
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
loop.run_in_executor(None, partial(handler, arguments)),
|
||||
timeout=TOOL_TIMEOUT
|
||||
)
|
||||
return [TextContent(type="text", text=json.dumps(result, default=str, ensure_ascii=False))]
|
||||
except asyncio.TimeoutError:
|
||||
last_error = f"Tool '{name}' timed out after {TOOL_TIMEOUT}s"
|
||||
except Exception as e:
|
||||
last_error = f"{e}"
|
||||
if attempt < TOOL_RETRIES:
|
||||
print(f"[Retry] Tool '{name}' failed ({last_error}), retrying...")
|
||||
return [TextContent(type="text", text=json.dumps({"error": last_error}))]
|
||||
|
||||
async def main():
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
|
||||
Reference in New Issue
Block a user