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:
nogueiraguh
2026-03-07 12:39:20 -03:00
parent 3e13cf4677
commit c7c4ca1f1b
5 changed files with 2032 additions and 197 deletions

View File

@@ -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):