feat: granular CIS MCP server, chat memory compaction, and UX improvements
- Rewrite mcp_cis_server.py with per-section scan tools (IAM, Networking, Compute, Logging/Monitoring, Storage, Asset Management) instead of monolithic full-tenancy scan — 12 granular tools - Add cis_reports.py (Oracle CIS Benchmark checker) to backend - Add chat memory compaction: auto-summarize old messages when history exceeds ~8000 tokens, keeping 6 recent messages intact - Fix GenAI tool use loop: accumulate assistant+tool messages across iterations for proper conversation flow (Generic format) - Remove tenancy confirmation section from system prompt - Add thinking indicator and button disable in chat UI while waiting - Add requests dependency for cis_reports.py - Fix NoneType error in cis_reports.py region filtering
This commit is contained in:
@@ -15,6 +15,8 @@ RUN pip install --no-cache-dir -r requirements.txt && \
|
|||||||
# Copy app
|
# Copy app
|
||||||
COPY app.py .
|
COPY app.py .
|
||||||
COPY cis_runner.py .
|
COPY cis_runner.py .
|
||||||
|
COPY cis_reports.py .
|
||||||
|
COPY mcp_cis_server.py .
|
||||||
|
|
||||||
# Data volume
|
# Data volume
|
||||||
RUN mkdir -p /data
|
RUN mkdir -p /data
|
||||||
|
|||||||
197
backend/app.py
197
backend/app.py
@@ -38,6 +38,12 @@ for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]:
|
|||||||
|
|
||||||
_running_reports: dict[str, asyncio.subprocess.Process] = {} # rid → subprocess
|
_running_reports: dict[str, asyncio.subprocess.Process] = {} # rid → subprocess
|
||||||
|
|
||||||
|
# ── Chat Memory Compaction Settings ──
|
||||||
|
COMPACT_TOKEN_THRESHOLD = 8000 # estimated tokens before triggering compaction
|
||||||
|
COMPACT_KEEP_RECENT = 6 # recent messages to keep uncompacted
|
||||||
|
COMPACT_SUMMARY_MAX_TOKENS = 1000
|
||||||
|
COMPACT_MIN_MESSAGES = 6
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
log = logging.getLogger("agent")
|
log = logging.getLogger("agent")
|
||||||
|
|
||||||
@@ -304,6 +310,15 @@ def init_db():
|
|||||||
model_id TEXT,
|
model_id TEXT,
|
||||||
created_at TEXT DEFAULT (datetime('now'))
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS chat_summaries (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
session_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
summary TEXT NOT NULL,
|
||||||
|
messages_compacted INTEGER NOT NULL,
|
||||||
|
up_to_message_id TEXT NOT NULL,
|
||||||
|
created_at TEXT DEFAULT (datetime('now'))
|
||||||
|
);
|
||||||
CREATE TABLE IF NOT EXISTS audit_log (
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
id TEXT PRIMARY KEY, user_id TEXT, username TEXT,
|
id TEXT PRIMARY KEY, user_id TEXT, username TEXT,
|
||||||
action TEXT NOT NULL, resource TEXT, details TEXT,
|
action TEXT NOT NULL, resource TEXT, details TEXT,
|
||||||
@@ -871,7 +886,7 @@ async def test_genai(gid: str, u=Depends(require("admin","user"))):
|
|||||||
if not gc: raise HTTPException(404)
|
if not gc: raise HTTPException(404)
|
||||||
gname = gc["name"]
|
gname = gc["name"]
|
||||||
try:
|
try:
|
||||||
resp, _ = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.")
|
resp, _, _ = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.")
|
||||||
_config_log("genai", gid, gname, "success", "test", f"GenAI OK: {resp[:200]}", u["id"], u["username"])
|
_config_log("genai", gid, gname, "success", "test", f"GenAI OK: {resp[:200]}", u["id"], u["username"])
|
||||||
return {"status":"success","message":"GenAI OK","response":resp[:300]}
|
return {"status":"success","message":"GenAI OK","response":resp[:300]}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -879,11 +894,118 @@ async def test_genai(gid: str, u=Depends(require("admin","user"))):
|
|||||||
_config_log("genai", gid, gname, "error", "test", msg, u["id"], u["username"])
|
_config_log("genai", gid, gname, "error", "test", msg, u["id"], u["username"])
|
||||||
return {"status":"error","message":msg}
|
return {"status":"error","message":msg}
|
||||||
|
|
||||||
|
# ── Chat Memory Compaction ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _estimate_tokens(text: str) -> int:
|
||||||
|
return len(text) // 4
|
||||||
|
|
||||||
|
def _estimate_history_tokens(history: list) -> int:
|
||||||
|
return sum(_estimate_tokens(h["content"]) for h in history)
|
||||||
|
|
||||||
|
def _should_compact(history: list) -> bool:
|
||||||
|
if len(history) < COMPACT_MIN_MESSAGES:
|
||||||
|
return False
|
||||||
|
return _estimate_history_tokens(history) > COMPACT_TOKEN_THRESHOLD
|
||||||
|
|
||||||
|
def _generate_summary(genai_cfg: dict, messages_to_summarize: list) -> str:
|
||||||
|
"""Use the same GenAI model to summarize older conversation messages."""
|
||||||
|
conversation_text = ""
|
||||||
|
for m in messages_to_summarize:
|
||||||
|
role_label = "Usuário" if m["role"] == "user" else "Assistente"
|
||||||
|
# Truncate very long messages in the summary input
|
||||||
|
content = m["content"][:3000] if len(m["content"]) > 3000 else m["content"]
|
||||||
|
conversation_text += f"{role_label}: {content}\n\n"
|
||||||
|
|
||||||
|
summary_prompt = (
|
||||||
|
"Resuma a conversa abaixo em um parágrafo conciso que capture:\n"
|
||||||
|
"- Tópicos principais discutidos\n"
|
||||||
|
"- Decisões tomadas e conclusões\n"
|
||||||
|
"- Resultados de ferramentas/tools executadas\n"
|
||||||
|
"- Contexto OCI/CIS relevante para perguntas futuras\n\n"
|
||||||
|
"Seja conciso mas preserve toda informação acionável.\n\n"
|
||||||
|
"CONVERSA:\n" + conversation_text
|
||||||
|
)
|
||||||
|
|
||||||
|
summary_cfg = dict(genai_cfg)
|
||||||
|
summary_cfg["system_prompt"] = "Você é um sumarizador. Responda apenas com o resumo."
|
||||||
|
summary_cfg["max_tokens"] = COMPACT_SUMMARY_MAX_TOKENS
|
||||||
|
|
||||||
|
try:
|
||||||
|
text, _, _ = _call_genai(summary_cfg, summary_prompt, history=None, tools=None)
|
||||||
|
return text.strip()
|
||||||
|
except Exception as e:
|
||||||
|
log.warning(f"Summary generation failed: {e}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _compact_history(session_id: str, user_id: str, genai_cfg: dict, history: list) -> list:
|
||||||
|
"""Compact conversation history by summarizing older messages."""
|
||||||
|
if len(history) <= COMPACT_KEEP_RECENT:
|
||||||
|
return history
|
||||||
|
|
||||||
|
messages_to_summarize = history[:-COMPACT_KEEP_RECENT]
|
||||||
|
recent_messages = history[-COMPACT_KEEP_RECENT:]
|
||||||
|
|
||||||
|
# Check for existing summary
|
||||||
|
with db() as c:
|
||||||
|
row = c.execute(
|
||||||
|
"SELECT summary, messages_compacted FROM chat_summaries "
|
||||||
|
"WHERE session_id=? ORDER BY created_at DESC LIMIT 1",
|
||||||
|
(session_id,)
|
||||||
|
).fetchone()
|
||||||
|
if row:
|
||||||
|
existing_summary = row["summary"]
|
||||||
|
prev_compacted = row["messages_compacted"]
|
||||||
|
# Reuse if no new messages to summarize
|
||||||
|
if len(messages_to_summarize) <= prev_compacted:
|
||||||
|
return [{"role": "assistant", "content": f"[Resumo da conversa anterior: {existing_summary}]"}] + recent_messages
|
||||||
|
else:
|
||||||
|
existing_summary = None
|
||||||
|
|
||||||
|
# Include existing summary as prefix for incremental compaction
|
||||||
|
if existing_summary:
|
||||||
|
messages_to_summarize = [{"role": "assistant", "content": f"[Resumo anterior: {existing_summary}]"}] + messages_to_summarize
|
||||||
|
|
||||||
|
summary_text = _generate_summary(genai_cfg, messages_to_summarize)
|
||||||
|
|
||||||
|
if not summary_text:
|
||||||
|
# Fallback: truncate keeping recent messages that fit in budget
|
||||||
|
truncated = []
|
||||||
|
budget = 6000
|
||||||
|
for m in reversed(history):
|
||||||
|
t = _estimate_tokens(m["content"])
|
||||||
|
if budget - t < 0:
|
||||||
|
break
|
||||||
|
truncated.insert(0, m)
|
||||||
|
budget -= t
|
||||||
|
return truncated
|
||||||
|
|
||||||
|
# Save summary to DB
|
||||||
|
actual_count = len(messages_to_summarize) - (1 if existing_summary else 0)
|
||||||
|
with db() as c:
|
||||||
|
last_msg = c.execute(
|
||||||
|
"SELECT id FROM chat_messages WHERE session_id=? AND role IN ('user','assistant') "
|
||||||
|
"ORDER BY created_at ASC LIMIT 1 OFFSET ?",
|
||||||
|
(session_id, max(0, actual_count - 1))
|
||||||
|
).fetchone()
|
||||||
|
last_id = last_msg["id"] if last_msg else "unknown"
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO chat_summaries (id,session_id,user_id,summary,messages_compacted,up_to_message_id) VALUES (?,?,?,?,?,?)",
|
||||||
|
(str(uuid.uuid4()), session_id, user_id, summary_text, actual_count, last_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info(f"Generated summary for session {session_id}: {len(summary_text)} chars, {actual_count} msgs compacted")
|
||||||
|
return [{"role": "assistant", "content": f"[Resumo da conversa anterior: {summary_text}]"}] + recent_messages
|
||||||
|
|
||||||
|
# ── GenAI Call ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _call_genai(gc: dict, message: str, history: list = None, tools: list = None,
|
def _call_genai(gc: dict, message: str, history: list = None, tools: list = None,
|
||||||
tool_results_cohere: list = None, tool_results_generic: list = None) -> tuple:
|
tool_results_cohere: list = None,
|
||||||
|
extra_messages: list = None) -> tuple:
|
||||||
"""
|
"""
|
||||||
Call OCI Generative AI with optional tool use support.
|
Call OCI Generative AI with optional tool use support.
|
||||||
Returns (text, tool_calls) tuple. tool_calls is a list of dicts or None.
|
Returns (text, tool_calls, tool_calls_raw) tuple.
|
||||||
|
tool_calls is a list of dicts or None. tool_calls_raw is the raw OCI SDK objects (for Generic format continuations).
|
||||||
|
extra_messages: list of raw OCI SDK message objects to append after the user message (for tool use loop accumulation).
|
||||||
"""
|
"""
|
||||||
import oci
|
import oci
|
||||||
|
|
||||||
@@ -992,16 +1114,6 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None
|
|||||||
msg.content = [content]
|
msg.content = [content]
|
||||||
messages.append(msg)
|
messages.append(msg)
|
||||||
|
|
||||||
# Tool results from previous iteration (Generic format)
|
|
||||||
if tool_results_generic:
|
|
||||||
for tr in tool_results_generic:
|
|
||||||
tool_msg = oci.generative_ai_inference.models.ToolMessage()
|
|
||||||
tool_msg.tool_call_id = tr["tool_call_id"]
|
|
||||||
tool_content = oci.generative_ai_inference.models.TextContent()
|
|
||||||
tool_content.text = tr["content"]
|
|
||||||
tool_msg.content = [tool_content]
|
|
||||||
messages.append(tool_msg)
|
|
||||||
|
|
||||||
# Current user message
|
# Current user message
|
||||||
content = oci.generative_ai_inference.models.TextContent()
|
content = oci.generative_ai_inference.models.TextContent()
|
||||||
content.text = message
|
content.text = message
|
||||||
@@ -1010,6 +1122,10 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None
|
|||||||
user_message.content = [content]
|
user_message.content = [content]
|
||||||
messages.append(user_message)
|
messages.append(user_message)
|
||||||
|
|
||||||
|
# Append accumulated tool use loop messages (assistant+tool_calls → tool_results → ...)
|
||||||
|
if extra_messages:
|
||||||
|
messages.extend(extra_messages)
|
||||||
|
|
||||||
chat_request.messages = messages
|
chat_request.messages = messages
|
||||||
|
|
||||||
# Tool definitions for Generic format
|
# Tool definitions for Generic format
|
||||||
@@ -1058,10 +1174,11 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None
|
|||||||
"name": tc.name,
|
"name": tc.name,
|
||||||
"arguments": tc.parameters if isinstance(tc.parameters, dict) else {}
|
"arguments": tc.parameters if isinstance(tc.parameters, dict) else {}
|
||||||
})
|
})
|
||||||
return (text or "", tool_calls)
|
return (text or "", tool_calls, None)
|
||||||
else:
|
else:
|
||||||
text = ""
|
text = ""
|
||||||
tool_calls = None
|
tool_calls = None
|
||||||
|
tool_calls_raw = None # raw content list from assistant message for continuation
|
||||||
if hasattr(resp, 'choices') and resp.choices:
|
if hasattr(resp, 'choices') and resp.choices:
|
||||||
choice = resp.choices[0]
|
choice = resp.choices[0]
|
||||||
if hasattr(choice, 'message') and choice.message:
|
if hasattr(choice, 'message') and choice.message:
|
||||||
@@ -1078,12 +1195,14 @@ def _call_genai(gc: dict, message: str, history: list = None, tools: list = None
|
|||||||
"name": tc.name if hasattr(tc, 'name') else "",
|
"name": tc.name if hasattr(tc, 'name') else "",
|
||||||
"arguments": args
|
"arguments": args
|
||||||
})
|
})
|
||||||
|
# Preserve raw tool_calls for building assistant message in next iteration
|
||||||
|
tool_calls_raw = choice.message.tool_calls
|
||||||
# Extract text
|
# Extract text
|
||||||
if hasattr(choice.message, 'content') and choice.message.content:
|
if hasattr(choice.message, 'content') and choice.message.content:
|
||||||
contents = choice.message.content
|
contents = choice.message.content
|
||||||
if contents and len(contents) > 0:
|
if contents and len(contents) > 0:
|
||||||
text = contents[0].text if hasattr(contents[0], 'text') else ""
|
text = contents[0].text if hasattr(contents[0], 'text') else ""
|
||||||
return (text or "", tool_calls)
|
return (text or "", tool_calls, tool_calls_raw)
|
||||||
|
|
||||||
# ── RAG Helpers ───────────────────────────────────────────────────────────────
|
# ── RAG Helpers ───────────────────────────────────────────────────────────────
|
||||||
RAG_CONTEXT_TEMPLATE = """--- CONTEXTO RECUPERADO (Vector Search) ---
|
RAG_CONTEXT_TEMPLATE = """--- CONTEXTO RECUPERADO (Vector Search) ---
|
||||||
@@ -1098,11 +1217,6 @@ RAG_DEFAULT_SYSTEM_PROMPT = """Você é um assistente RAG especializado em Oracl
|
|||||||
- Responda **SOMENTE** perguntas relacionadas a Oracle Cloud (OCI), CIS Benchmark, CIS Report e itens de inventário OCI (IAM/AssetManagement/Networking/StorageBlock/FileStorageService/Objectstore/Compute/LoggingandMonitoring).
|
- Responda **SOMENTE** perguntas relacionadas a Oracle Cloud (OCI), CIS Benchmark, CIS Report e itens de inventário OCI (IAM/AssetManagement/Networking/StorageBlock/FileStorageService/Objectstore/Compute/LoggingandMonitoring).
|
||||||
- Se a pergunta não for desse escopo, recuse educadamente e peça uma pergunta dentro do tema.
|
- Se a pergunta não for desse escopo, recuse educadamente e peça uma pergunta dentro do tema.
|
||||||
|
|
||||||
### Tenancy (obrigatório)
|
|
||||||
1) Confirme a **TENANCY** alvo antes de responder, a menos que já esteja definida na conversa.
|
|
||||||
2) Se a tenancy ainda não estiver clara, pergunte uma única vez: "Qual tenancy devo considerar? (ex.: MinhaTenancy)" e aguarde a resposta.
|
|
||||||
3) Se o usuário trocar a tenancy, considere a nova tenancy como a atual.
|
|
||||||
|
|
||||||
### Bases vetorizadas disponíveis (contexto recuperado automaticamente)
|
### Bases vetorizadas disponíveis (contexto recuperado automaticamente)
|
||||||
- **CIS_REPORT**: report coletado na tenancy (compliance, findings, critérios/auditoria, evidências).
|
- **CIS_REPORT**: report coletado na tenancy (compliance, findings, critérios/auditoria, evidências).
|
||||||
- **CIS_RECOMMENDATIONS**: recomendações práticas de correção/remediação para itens não compliant.
|
- **CIS_RECOMMENDATIONS**: recomendações práticas de correção/remediação para itens não compliant.
|
||||||
@@ -2101,11 +2215,19 @@ async def chat(msg: ChatMsg, u=Depends(current_user)):
|
|||||||
tool_defs = [t["tool"] for t in mcp_tools]
|
tool_defs = [t["tool"] for t in mcp_tools]
|
||||||
log.info(f"Chat with {len(tool_defs)} MCP tools available")
|
log.info(f"Chat with {len(tool_defs)} MCP tools available")
|
||||||
|
|
||||||
|
# ── Memory compaction: summarize old messages if history is too long ──
|
||||||
|
if history and _should_compact(history):
|
||||||
|
log.info(f"Compaction triggered: {len(history)} msgs, ~{_estimate_history_tokens(history)} est tokens")
|
||||||
|
history = _compact_history(sid, u["id"], cfg_dict, history)
|
||||||
|
log.info(f"Post-compaction: {len(history)} msgs, ~{_estimate_history_tokens(history)} est tokens")
|
||||||
|
|
||||||
hist = history[:-1] if len(history) > 1 else None
|
hist = history[:-1] if len(history) > 1 else None
|
||||||
resp_text, tool_calls = _call_genai(cfg_dict, augmented_message, hist, tools=tool_defs)
|
resp_text, tool_calls, tool_calls_raw = _call_genai(cfg_dict, augmented_message, hist, tools=tool_defs)
|
||||||
|
|
||||||
# Tool use loop (max 5 iterations)
|
# Tool use loop (max 5 iterations)
|
||||||
|
# Accumulate extra_messages so the model sees the full tool use conversation
|
||||||
all_tool_results = []
|
all_tool_results = []
|
||||||
|
accumulated_msgs = [] # raw OCI SDK message objects for Generic format
|
||||||
iterations = 0
|
iterations = 0
|
||||||
api_format = GENAI_MODELS.get(genai_cfg.get("model_id", ""), {}).get("api_format", "GENERIC")
|
api_format = GENAI_MODELS.get(genai_cfg.get("model_id", ""), {}).get("api_format", "GENERIC")
|
||||||
while tool_calls and iterations < 5:
|
while tool_calls and iterations < 5:
|
||||||
@@ -2138,14 +2260,26 @@ async def chat(msg: ChatMsg, u=Depends(current_user)):
|
|||||||
tr_obj.call = tc_obj
|
tr_obj.call = tc_obj
|
||||||
tr_obj.outputs = [{"result": tr["content"]}]
|
tr_obj.outputs = [{"result": tr["content"]}]
|
||||||
cohere_results.append(tr_obj)
|
cohere_results.append(tr_obj)
|
||||||
resp_text, tool_calls = _call_genai(
|
resp_text, tool_calls, tool_calls_raw = _call_genai(
|
||||||
cfg_dict, augmented_message, hist, tools=tool_defs,
|
cfg_dict, augmented_message, hist, tools=tool_defs,
|
||||||
tool_results_cohere=cohere_results)
|
tool_results_cohere=cohere_results)
|
||||||
else:
|
else:
|
||||||
generic_results = [{"tool_call_id": tr["tool_call_id"], "content": tr["content"]} for tr in iteration_results]
|
import oci
|
||||||
resp_text, tool_calls = _call_genai(
|
# Accumulate: add assistant message with tool_calls + tool result messages
|
||||||
|
assistant_msg = oci.generative_ai_inference.models.AssistantMessage()
|
||||||
|
assistant_msg.tool_calls = tool_calls_raw
|
||||||
|
accumulated_msgs.append(assistant_msg)
|
||||||
|
for tr in iteration_results:
|
||||||
|
tool_msg = oci.generative_ai_inference.models.ToolMessage()
|
||||||
|
tool_msg.tool_call_id = tr["tool_call_id"]
|
||||||
|
tool_content = oci.generative_ai_inference.models.TextContent()
|
||||||
|
tool_content.text = tr["content"]
|
||||||
|
tool_msg.content = [tool_content]
|
||||||
|
accumulated_msgs.append(tool_msg)
|
||||||
|
|
||||||
|
resp_text, tool_calls, tool_calls_raw = _call_genai(
|
||||||
cfg_dict, augmented_message, hist, tools=tool_defs,
|
cfg_dict, augmented_message, hist, tools=tool_defs,
|
||||||
tool_results_generic=generic_results)
|
extra_messages=accumulated_msgs)
|
||||||
|
|
||||||
resp = resp_text
|
resp = resp_text
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2324,6 +2458,19 @@ async def startup():
|
|||||||
orphaned = c.execute("UPDATE reports SET status='failed', error_msg='Interrompido: container reiniciado' WHERE status='running'").rowcount
|
orphaned = c.execute("UPDATE reports SET status='failed', error_msg='Interrompido: container reiniciado' WHERE status='running'").rowcount
|
||||||
if orphaned:
|
if orphaned:
|
||||||
log.warning(f"Marked {orphaned} orphaned running report(s) as failed")
|
log.warning(f"Marked {orphaned} orphaned running report(s) as failed")
|
||||||
|
# Auto-register CIS Compliance Scanner MCP server if not present
|
||||||
|
with db() as c:
|
||||||
|
existing = c.execute("SELECT id FROM mcp_servers WHERE name='CIS Compliance Scanner'").fetchone()
|
||||||
|
if not existing:
|
||||||
|
mid = str(uuid.uuid4())
|
||||||
|
c.execute(
|
||||||
|
"INSERT INTO mcp_servers (id, user_id, name, description, server_type, command, args) VALUES (?,?,?,?,?,?,?)",
|
||||||
|
(mid, "system", "CIS Compliance Scanner",
|
||||||
|
"OCI CIS Foundations Benchmark 3.0 - 48 CIS checks + 11 OBP checks via MCP",
|
||||||
|
"stdio", "python3", json.dumps(["/app/mcp_cis_server.py"]))
|
||||||
|
)
|
||||||
|
log.info(f"Auto-registered CIS Compliance Scanner MCP server (id={mid})")
|
||||||
|
|
||||||
log.info(f"OCI CIS AI Agent v{VERSION} API started")
|
log.info(f"OCI CIS AI Agent v{VERSION} API started")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
6660
backend/cis_reports.py
Normal file
6660
backend/cis_reports.py
Normal file
File diff suppressed because one or more lines are too long
613
backend/mcp_cis_server.py
Normal file
613
backend/mcp_cis_server.py
Normal file
@@ -0,0 +1,613 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
MCP Server: CIS OCI Foundations Benchmark 3.0 Compliance Scanner
|
||||||
|
|
||||||
|
Granular per-section data collection — each section only fetches the OCI data
|
||||||
|
it needs, avoiding the slow full-tenancy scan.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import concurrent.futures
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from functools import partial
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from mcp.server import Server
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
from mcp.types import Tool, TextContent
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
server = Server("cis-compliance")
|
||||||
|
|
||||||
|
# config_id -> {checker, sections_collected: set, sections_analyzed: set, ...}
|
||||||
|
_sessions: dict = {}
|
||||||
|
SESSION_TTL = 1800
|
||||||
|
|
||||||
|
OCI_CONFIGS_DIR = os.environ.get("OCI_CONFIGS_DIR", "/data/oci_configs")
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Per-section data collectors: which private methods to call
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Home-region functions (run once, needed by most sections)
|
||||||
|
HOME_BASE = [
|
||||||
|
"_CIS_Report__identity_read_compartments",
|
||||||
|
"_CIS_Report__cloud_guard_read_cloud_guard_configuration",
|
||||||
|
"_CIS_Report__identity_read_domains",
|
||||||
|
"_CIS_Report__identity_read_groups",
|
||||||
|
"_CIS_Report__identity_read_availability_domains",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Section -> (home_region_collectors, regional_collectors, analysis_methods)
|
||||||
|
SECTION_DEPS = {
|
||||||
|
"iam": {
|
||||||
|
"home": [
|
||||||
|
"_CIS_Report__identity_read_users",
|
||||||
|
"_CIS_Report__identity_read_tenancy_password_policy",
|
||||||
|
"_CIS_Report__identity_read_dynamic_groups",
|
||||||
|
"_CIS_Report__identity_read_tenancy_policies",
|
||||||
|
],
|
||||||
|
"regional": [],
|
||||||
|
"analyze": [
|
||||||
|
"_CIS_Report__cis_check_iam_policies",
|
||||||
|
"_CIS_Report__cis_check_password_policies",
|
||||||
|
"_CIS_Report__cis_check_users",
|
||||||
|
"_CIS_Report__cis_check_dynamic_groups",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"networking": {
|
||||||
|
"home": [],
|
||||||
|
"regional": [
|
||||||
|
"_CIS_Report__network_read_network_security_lists",
|
||||||
|
"_CIS_Report__network_read_network_security_groups_rules",
|
||||||
|
"_CIS_Report__network_read_network_vcns",
|
||||||
|
"_CIS_Report__network_read_network_subnets",
|
||||||
|
"_CIS_Report__adb_read_adbs",
|
||||||
|
"_CIS_Report__oic_read_oics",
|
||||||
|
"_CIS_Report__oac_read_oacs",
|
||||||
|
],
|
||||||
|
"analyze": ["_CIS_Report__cis_check_network_security"],
|
||||||
|
},
|
||||||
|
"compute": {
|
||||||
|
"home": [],
|
||||||
|
"regional": ["_CIS_Report__core_instance_read_compute"],
|
||||||
|
"analyze": ["_CIS_Report__cis_check_compute_instances"],
|
||||||
|
},
|
||||||
|
"logging_monitoring": {
|
||||||
|
"home": [
|
||||||
|
"_CIS_Report__identity_read_tag_defaults",
|
||||||
|
"_CIS_Report__identity_read_tenancy_policies",
|
||||||
|
],
|
||||||
|
"regional": [
|
||||||
|
"_CIS_Report__events_read_event_rules",
|
||||||
|
"_CIS_Report__ons_read_subscriptions",
|
||||||
|
"_CIS_Report__logging_read_log_groups_and_logs",
|
||||||
|
"_CIS_Report__kms_read_keys",
|
||||||
|
"_CIS_Report__os_read_buckets",
|
||||||
|
"_CIS_Report__network_read_network_subnets",
|
||||||
|
"_CIS_Report__network_read_network_capturefilters",
|
||||||
|
],
|
||||||
|
"analyze": ["_CIS_Report__cis_check_tagging_and_monitoring"],
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"home": [],
|
||||||
|
"regional": [
|
||||||
|
"_CIS_Report__os_read_buckets",
|
||||||
|
"_CIS_Report__block_volume_read_block_volumes",
|
||||||
|
"_CIS_Report__boot_volume_read_boot_volumes",
|
||||||
|
"_CIS_Report__fss_read_fsss",
|
||||||
|
],
|
||||||
|
"analyze": ["_CIS_Report__cis_check_storage"],
|
||||||
|
},
|
||||||
|
"asset_management": {
|
||||||
|
"home": [],
|
||||||
|
"regional": ["_CIS_Report__search_resources_in_root_compartment"],
|
||||||
|
"analyze": ["_CIS_Report__cis_check_assets"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION_NAMES = {
|
||||||
|
"iam": "Identity and Access Management",
|
||||||
|
"networking": "Networking",
|
||||||
|
"compute": "Compute",
|
||||||
|
"logging_monitoring": "Logging and Monitoring",
|
||||||
|
"storage": "Storage",
|
||||||
|
"asset_management": "Asset Management",
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION_MAP = {"1": "iam", "2": "networking", "3": "compute", "4": "logging_monitoring", "5": "storage", "6": "asset_management"}
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Helpers
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _serialize_oci_obj(obj, max_depth=3):
|
||||||
|
if max_depth <= 0: return str(obj)
|
||||||
|
if obj is None: return None
|
||||||
|
if isinstance(obj, (str, int, float, bool)): return obj
|
||||||
|
if isinstance(obj, datetime.datetime): return obj.isoformat()
|
||||||
|
if isinstance(obj, (list, tuple)): return [_serialize_oci_obj(i, max_depth-1) for i in obj]
|
||||||
|
if isinstance(obj, dict): return {k: _serialize_oci_obj(v, max_depth-1) for k, v in obj.items()}
|
||||||
|
if hasattr(obj, "swagger_types"):
|
||||||
|
return {a: _serialize_oci_obj(getattr(obj, a, None), max_depth-1) for a in obj.swagger_types}
|
||||||
|
return str(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_config_path(config_id: str) -> str:
|
||||||
|
return os.path.join(OCI_CONFIGS_DIR, config_id, "config")
|
||||||
|
|
||||||
|
|
||||||
|
def _list_available_configs() -> list[dict]:
|
||||||
|
configs = []
|
||||||
|
if not os.path.isdir(OCI_CONFIGS_DIR): return configs
|
||||||
|
for entry in sorted(os.listdir(OCI_CONFIGS_DIR)):
|
||||||
|
config_path = os.path.join(OCI_CONFIGS_DIR, entry, "config")
|
||||||
|
if os.path.isfile(config_path):
|
||||||
|
info = {"config_id": entry, "config_path": config_path}
|
||||||
|
try:
|
||||||
|
cp = ConfigParser(); cp.read(config_path)
|
||||||
|
sec = cp.sections()[0] if cp.sections() else "DEFAULT"
|
||||||
|
info["tenancy_ocid"] = cp.get(sec, "tenancy", fallback="N/A")
|
||||||
|
info["region"] = cp.get(sec, "region", fallback="N/A")
|
||||||
|
info["user_ocid"] = cp.get(sec, "user", fallback="N/A")
|
||||||
|
except Exception: pass
|
||||||
|
configs.append(info)
|
||||||
|
return configs
|
||||||
|
|
||||||
|
|
||||||
|
def _get_or_create_checker(config_id: str, regions: list[str] | None = None):
|
||||||
|
"""Return cached session or create a new CIS_Report instance (fast — no data collection)."""
|
||||||
|
now = time.time()
|
||||||
|
if config_id in _sessions:
|
||||||
|
s = _sessions[config_id]
|
||||||
|
if now - s["created_at"] < SESSION_TTL:
|
||||||
|
return s
|
||||||
|
del _sessions[config_id]
|
||||||
|
|
||||||
|
import cis_reports
|
||||||
|
|
||||||
|
config_path = _get_config_path(config_id)
|
||||||
|
if not os.path.isfile(config_path):
|
||||||
|
raise FileNotFoundError(f"OCI config not found: {config_path}")
|
||||||
|
|
||||||
|
cp = ConfigParser(); cp.read(config_path)
|
||||||
|
profile = cp.sections()[0] if cp.sections() else "DEFAULT"
|
||||||
|
|
||||||
|
config, signer = cis_reports.create_signer(
|
||||||
|
file_location=config_path, config_profile=profile,
|
||||||
|
is_instance_principals=False, is_delegation_token=False, is_security_token=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
regions_str = ",".join(regions) if regions else None
|
||||||
|
|
||||||
|
checker = cis_reports.CIS_Report(
|
||||||
|
config=config, signer=signer, proxy=None, output_bucket=None,
|
||||||
|
report_directory="/tmp/cis_reports", report_prefix="",
|
||||||
|
report_summary_json=False, print_to_screen="FALSE",
|
||||||
|
regions_to_run_in=regions_str, raw_data=False, obp=False,
|
||||||
|
redact_output=False, debug=False, all_resources=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
session = {
|
||||||
|
"checker": checker, "created_at": now, "config_id": config_id,
|
||||||
|
"base_collected": False, "sections_collected": set(), "sections_analyzed": set(),
|
||||||
|
}
|
||||||
|
_sessions[config_id] = session
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_base(session):
|
||||||
|
"""Collect compartments, groups, domains — shared prereqs for all sections."""
|
||||||
|
if session["base_collected"]:
|
||||||
|
return
|
||||||
|
checker = session["checker"]
|
||||||
|
# Sequential: compartments + cloud_guard first, then domains, then groups
|
||||||
|
getattr(checker, "_CIS_Report__identity_read_compartments")()
|
||||||
|
getattr(checker, "_CIS_Report__cloud_guard_read_cloud_guard_configuration")()
|
||||||
|
getattr(checker, "_CIS_Report__identity_read_domains")()
|
||||||
|
getattr(checker, "_CIS_Report__identity_read_groups")()
|
||||||
|
getattr(checker, "_CIS_Report__identity_read_availability_domains")()
|
||||||
|
session["base_collected"] = True
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_section(session, section: str):
|
||||||
|
"""Collect data for a specific section only."""
|
||||||
|
if section in session["sections_collected"]:
|
||||||
|
return
|
||||||
|
_ensure_base(session)
|
||||||
|
checker = session["checker"]
|
||||||
|
deps = SECTION_DEPS.get(section)
|
||||||
|
if not deps:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Home region collectors (sequential for safety)
|
||||||
|
for method_name in deps["home"]:
|
||||||
|
try:
|
||||||
|
getattr(checker, method_name)()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: {method_name} failed: {e}")
|
||||||
|
|
||||||
|
# Regional collectors (parallel with thread pool)
|
||||||
|
if deps["regional"]:
|
||||||
|
def _run(fn_name):
|
||||||
|
try:
|
||||||
|
getattr(checker, fn_name)()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: {fn_name} failed: {e}")
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as ex:
|
||||||
|
list(ex.map(_run, deps["regional"]))
|
||||||
|
|
||||||
|
session["sections_collected"].add(section)
|
||||||
|
|
||||||
|
|
||||||
|
def _analyze_section(session, section: str):
|
||||||
|
"""Run CIS analysis for a specific section."""
|
||||||
|
if section in session["sections_analyzed"]:
|
||||||
|
return
|
||||||
|
_collect_section(session, section)
|
||||||
|
checker = session["checker"]
|
||||||
|
deps = SECTION_DEPS.get(section)
|
||||||
|
if not deps:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Initialize regional findings data if not set
|
||||||
|
if not hasattr(checker, '_CIS_Report__cis_regional_findings_data') or not checker._CIS_Report__cis_regional_findings_data:
|
||||||
|
checker._CIS_Report__cis_regional_findings_data = {}
|
||||||
|
cis_regional_checks = checker._CIS_Report__cis_regional_checks
|
||||||
|
for check in cis_regional_checks:
|
||||||
|
checker._CIS_Report__cis_regional_findings_data[check] = {}
|
||||||
|
for rk in checker._CIS_Report__regions:
|
||||||
|
checker._CIS_Report__cis_regional_findings_data[check][rk] = None
|
||||||
|
|
||||||
|
for method_name in deps["analyze"]:
|
||||||
|
try:
|
||||||
|
getattr(checker, method_name)()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Warning: {method_name} failed: {e}")
|
||||||
|
|
||||||
|
session["sections_analyzed"].add(section)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_id_to_section(check_id: str) -> str:
|
||||||
|
prefix = check_id.split(".")[0]
|
||||||
|
return SECTION_MAP.get(prefix, "unknown")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_benchmark(checker):
|
||||||
|
return checker.cis_foundations_benchmark_3_0
|
||||||
|
|
||||||
|
|
||||||
|
def _section_summary(checker, section_key: str) -> dict:
|
||||||
|
benchmark = _get_benchmark(checker)
|
||||||
|
checks = []
|
||||||
|
passed = failed = 0
|
||||||
|
for rec_id, check in benchmark.items():
|
||||||
|
if _check_id_to_section(rec_id) != section_key:
|
||||||
|
continue
|
||||||
|
status = check.get("Status")
|
||||||
|
if status is True: passed += 1
|
||||||
|
elif status is False: failed += 1
|
||||||
|
checks.append({
|
||||||
|
"check_id": rec_id, "id": check["id"], "title": check["Title"],
|
||||||
|
"status": "PASS" if status is True else ("FAIL" if status is False else "N/A"),
|
||||||
|
"level": check.get("Level", "N/A"),
|
||||||
|
"findings_count": len(check.get("Findings", [])),
|
||||||
|
"findings": _serialize_oci_obj(check.get("Findings", [])[:20]),
|
||||||
|
})
|
||||||
|
total = passed + failed
|
||||||
|
score = round(passed / total * 100, 1) if total > 0 else 0
|
||||||
|
return {"section": SECTION_NAMES.get(section_key, section_key), "compliance_score": score,
|
||||||
|
"passed": passed, "failed": failed, "checks": checks}
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Remediation Data
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
REMEDIATIONS = {
|
||||||
|
"1.1": {"description": "Ensure service level admins are created to manage resources of particular service.", "steps": ["Create groups for each service (e.g., VolumeAdmins, ComputeAdmins)", "Write IAM policies granting each group manage access only to their service"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/policieshow/how-policies-work.htm"},
|
||||||
|
"1.2": {"description": "Ensure permissions on all resources are given only to the tenancy administrator group.", "steps": ["Review policies with 'manage all-resources in tenancy'", "Remove policies granting full access to non-Administrators groups"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/policieshow/how-policies-work.htm"},
|
||||||
|
"1.3": {"description": "Ensure IAM administrators cannot update tenancy Administrators group.", "steps": ["Add condition 'where target.group.name != Administrators' to IAM admin policies"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/policieshow/common-policies.htm"},
|
||||||
|
"1.4": {"description": "Ensure IAM password policy requires minimum length of 14 or greater.", "steps": ["Identity > Authentication Settings > Set minimum password length to 14+"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingpasswordrules.htm"},
|
||||||
|
"1.5": {"description": "Ensure IAM password policy expires passwords within 365 days.", "steps": ["Identity > Authentication Settings > Set expiration to 365 days or less"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingpasswordrules.htm"},
|
||||||
|
"1.6": {"description": "Ensure IAM password policy prevents password reuse.", "steps": ["Identity > Authentication Settings > Enable reuse prevention"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingpasswordrules.htm"},
|
||||||
|
"1.7": {"description": "Ensure MFA is enabled for all users with a console password.", "steps": ["Enable MFA for each user with console access", "Enable MFA enforcement in Authentication Settings"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/usingmfa.htm"},
|
||||||
|
"1.8": {"description": "Ensure user API keys rotate within 90 days.", "steps": ["Identify API keys older than 90 days", "Generate new keys, delete old ones"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
|
||||||
|
"1.9": {"description": "Ensure user customer secret keys rotate within 90 days.", "steps": ["Identify secret keys older than 90 days", "Rotate and delete old keys"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
|
||||||
|
"1.10": {"description": "Ensure user auth tokens rotate within 90 days.", "steps": ["Identify auth tokens older than 90 days", "Generate new, delete old"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
|
||||||
|
"1.11": {"description": "Ensure user IAM Database Passwords rotate within 90 days.", "steps": ["Identify DB passwords older than 90 days", "Reset passwords"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
|
||||||
|
"1.12": {"description": "Ensure API keys are not created for tenancy administrator users.", "steps": ["Remove API keys from Administrators group members", "Use instance principals for automation"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
|
||||||
|
"1.13": {"description": "Ensure all IAM users have a valid email address.", "steps": ["Review users and ensure valid email configured"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingusers.htm"},
|
||||||
|
"1.14": {"description": "Ensure Instance Principal authentication is used for instances, ADB and Functions.", "steps": ["Create dynamic groups", "Write policies for dynamic groups", "Update apps to use instance principals"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm"},
|
||||||
|
"1.15": {"description": "Ensure storage admins cannot delete resources they manage.", "steps": ["Add 'where request.permission != *_DELETE' conditions"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/policieshow/common-policies.htm"},
|
||||||
|
"1.16": {"description": "Ensure credentials unused for 45+ days are disabled.", "steps": ["Identify inactive users", "Disable accounts and credentials"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingusers.htm"},
|
||||||
|
"1.17": {"description": "Ensure only one active API Key per user.", "steps": ["Review users with multiple active API keys", "Remove extras"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcredentials.htm"},
|
||||||
|
"2.1": {"description": "No security lists allow ingress from 0.0.0.0/0 to port 22.", "steps": ["Review security lists for SSH rules", "Remove or restrict 0.0.0.0/0 to port 22"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/securitylists.htm"},
|
||||||
|
"2.2": {"description": "No security lists allow ingress from 0.0.0.0/0 to port 3389.", "steps": ["Remove or restrict 0.0.0.0/0 to port 3389"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/securitylists.htm"},
|
||||||
|
"2.3": {"description": "No NSGs allow ingress from 0.0.0.0/0 to port 22.", "steps": ["Review and restrict NSG SSH rules"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/networksecuritygroups.htm"},
|
||||||
|
"2.4": {"description": "No NSGs allow ingress from 0.0.0.0/0 to port 3389.", "steps": ["Review and restrict NSG RDP rules"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/networksecuritygroups.htm"},
|
||||||
|
"2.5": {"description": "Default security list restricts all traffic except ICMP.", "steps": ["Remove permissive rules from default security list", "Allow only ICMP within VCN CIDR"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/securitylists.htm"},
|
||||||
|
"2.6": {"description": "OIC access is restricted to allowed sources.", "steps": ["Configure OIC network access restrictions"], "oci_docs": "https://docs.oracle.com/en-us/iaas/integration/doc/restricting-network-access.html"},
|
||||||
|
"2.7": {"description": "OAC access is restricted or deployed within a VCN.", "steps": ["Deploy OAC with private access channel"], "oci_docs": "https://docs.oracle.com/en-us/iaas/analytics-cloud/doc/manage-network-access.html"},
|
||||||
|
"2.8": {"description": "ADB access is restricted or within a VCN.", "steps": ["Configure ADB with private endpoint"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Database/Concepts/adbsprivateaccess.htm"},
|
||||||
|
"3.1": {"description": "Compute Legacy Metadata endpoint is disabled.", "steps": ["Set 'are-legacy-imds-endpoints-disabled' to true"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/gettingmetadata.htm"},
|
||||||
|
"3.2": {"description": "Secure Boot is enabled on Compute Instance.", "steps": ["Enable Secure Boot in launch options"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Compute/References/shielded-instances.htm"},
|
||||||
|
"3.3": {"description": "In-transit Encryption is enabled on Compute Instance.", "steps": ["Enable in-transit encryption in launch options"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Block/Concepts/intransitencryption.htm"},
|
||||||
|
"4.1": {"description": "Default tags are used on resources.", "steps": ["Create tag defaults on root compartment"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Tagging/Tasks/managingtagdefaults.htm"},
|
||||||
|
"4.2": {"description": "At least one notification topic and subscription exists.", "steps": ["Create ONS topic + subscription"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Notification/Tasks/managingtopicsandsubscriptions.htm"},
|
||||||
|
"4.3": {"description": "Notification for Identity Provider changes.", "steps": ["Create Events rule for IdP CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.4": {"description": "Notification for IdP group mapping changes.", "steps": ["Create Events rule for IdP mapping CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.5": {"description": "Notification for IAM group changes.", "steps": ["Create Events rule for group CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.6": {"description": "Notification for IAM policy changes.", "steps": ["Create Events rule for policy CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.7": {"description": "Notification for user changes.", "steps": ["Create Events rule for user CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.8": {"description": "Notification for VCN changes.", "steps": ["Create Events rule for VCN CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.9": {"description": "Notification for route table changes.", "steps": ["Create Events rule for route table CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.10": {"description": "Notification for security list changes.", "steps": ["Create Events rule for SL CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.11": {"description": "Notification for NSG changes.", "steps": ["Create Events rule for NSG CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.12": {"description": "Notification for network gateway changes.", "steps": ["Create Events rules for DRG/IGW/NGW/SGW CRUD"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.13": {"description": "VCN flow logging is enabled for all subnets.", "steps": ["Enable VCN flow logs on each subnet"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/vcn-flow-logs.htm"},
|
||||||
|
"4.14": {"description": "Cloud Guard is enabled in root compartment.", "steps": ["Enable Cloud Guard in root compartment"], "oci_docs": "https://docs.oracle.com/en-us/iaas/cloud-guard/using/index.htm"},
|
||||||
|
"4.15": {"description": "Notification for Cloud Guard problems.", "steps": ["Create Events rule for CG problems"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"4.16": {"description": "CMK is rotated at least annually.", "steps": ["Rotate keys older than 365 days"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/KeyManagement/Tasks/managingkeys.htm"},
|
||||||
|
"4.17": {"description": "Write level Object Storage logging enabled.", "steps": ["Enable write logs for each bucket"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Logging/Concepts/service_logs.htm"},
|
||||||
|
"4.18": {"description": "Notification for Local OCI User Authentication.", "steps": ["Create Events rule for interactive login"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Events/Task/managingrules.htm"},
|
||||||
|
"5.1.1": {"description": "No Object Storage buckets are publicly visible.", "steps": ["Set all buckets to NoPublicAccess"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/managingbuckets.htm"},
|
||||||
|
"5.1.2": {"description": "Object Storage encrypted with CMK.", "steps": ["Assign CMK to each bucket"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/encryption.htm"},
|
||||||
|
"5.1.3": {"description": "Versioning enabled for Object Storage.", "steps": ["Enable versioning on each bucket"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Object/Tasks/usingversioning.htm"},
|
||||||
|
"5.2.1": {"description": "Block Volumes encrypted with CMK.", "steps": ["Assign CMK to each block volume"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Block/Concepts/blockvolumeencryption.htm"},
|
||||||
|
"5.2.2": {"description": "Boot Volumes encrypted with CMK.", "steps": ["Assign CMK to each boot volume"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Block/Concepts/blockvolumeencryption.htm"},
|
||||||
|
"5.3.1": {"description": "File Storage encrypted with CMK.", "steps": ["Assign CMK to each file system"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/File/Concepts/filestorageoverview.htm"},
|
||||||
|
"6.1": {"description": "At least one compartment exists.", "steps": ["Create compartments for organizing resources"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcompartments.htm"},
|
||||||
|
"6.2": {"description": "No resources in root compartment.", "steps": ["Move resources to child compartments"], "oci_docs": "https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingcompartments.htm"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static checks catalog
|
||||||
|
CHECKS_CATALOG = {
|
||||||
|
"1.1": ("IAM-1", "Identity and Access Management", "Ensure service level admins are created to manage resources of particular service", 1),
|
||||||
|
"1.2": ("IAM-2", "Identity and Access Management", "Ensure permissions on all resources are given only to the tenancy administrator group", 1),
|
||||||
|
"1.3": ("IAM-3", "Identity and Access Management", "Ensure IAM administrators cannot update tenancy Administrators group", 1),
|
||||||
|
"1.4": ("IAM-4", "Identity and Access Management", "Ensure IAM password policy requires minimum length of 14 or greater", 1),
|
||||||
|
"1.5": ("IAM-5", "Identity and Access Management", "Ensure IAM password policy expires passwords within 365 days", 1),
|
||||||
|
"1.6": ("IAM-6", "Identity and Access Management", "Ensure IAM password policy prevents password reuse", 1),
|
||||||
|
"1.7": ("IAM-7", "Identity and Access Management", "Ensure MFA is enabled for all users with a console password", 1),
|
||||||
|
"1.8": ("IAM-8", "Identity and Access Management", "Ensure user API keys rotate within 90 days or less", 1),
|
||||||
|
"1.9": ("IAM-9", "Identity and Access Management", "Ensure user customer secret keys rotate within 90 days or less", 1),
|
||||||
|
"1.10": ("IAM-10", "Identity and Access Management", "Ensure user auth tokens rotate within 90 days or less", 1),
|
||||||
|
"1.11": ("IAM-11", "Identity and Access Management", "Ensure user IAM Database Passwords rotate within 90 days", 1),
|
||||||
|
"1.12": ("IAM-12", "Identity and Access Management", "Ensure API keys are not created for tenancy administrator users", 1),
|
||||||
|
"1.13": ("IAM-13", "Identity and Access Management", "Ensure all OCI IAM user accounts have a valid and current email address", 1),
|
||||||
|
"1.14": ("IAM-14", "Identity and Access Management", "Ensure Instance Principal authentication is used", 1),
|
||||||
|
"1.15": ("IAM-15", "Identity and Access Management", "Ensure storage service-level admins cannot delete resources they manage", 2),
|
||||||
|
"1.16": ("IAM-16", "Identity and Access Management", "Ensure OCI IAM credentials unused for 45 days or more are disabled", 1),
|
||||||
|
"1.17": ("IAM-17", "Identity and Access Management", "Ensure there is only one active API Key for any single OCI IAM user", 1),
|
||||||
|
"2.1": ("NTW-1", "Networking", "No security lists allow ingress from 0.0.0.0/0 to port 22", 1),
|
||||||
|
"2.2": ("NTW-2", "Networking", "No security lists allow ingress from 0.0.0.0/0 to port 3389", 1),
|
||||||
|
"2.3": ("NTW-3", "Networking", "No NSGs allow ingress from 0.0.0.0/0 to port 22", 1),
|
||||||
|
"2.4": ("NTW-4", "Networking", "No NSGs allow ingress from 0.0.0.0/0 to port 3389", 1),
|
||||||
|
"2.5": ("NTW-5", "Networking", "Default security list restricts all traffic except ICMP", 1),
|
||||||
|
"2.6": ("NTW-6", "Networking", "OIC access is restricted to allowed sources", 1),
|
||||||
|
"2.7": ("NTW-7", "Networking", "OAC access is restricted or deployed within a VCN", 1),
|
||||||
|
"2.8": ("NTW-8", "Networking", "ADB access is restricted or deployed within a VCN", 1),
|
||||||
|
"3.1": ("COM-1", "Compute", "Legacy Metadata service endpoint is disabled", 2),
|
||||||
|
"3.2": ("COM-2", "Compute", "Secure Boot is enabled on Compute Instance", 2),
|
||||||
|
"3.3": ("COM-3", "Compute", "In-transit Encryption is enabled on Compute Instance", 1),
|
||||||
|
"4.1": ("LAM-1", "Logging and Monitoring", "Default tags are used on resources", 1),
|
||||||
|
"4.2": ("LAM-2", "Logging and Monitoring", "At least one notification topic and subscription", 1),
|
||||||
|
"4.3": ("LAM-3", "Logging and Monitoring", "Notification for Identity Provider changes", 1),
|
||||||
|
"4.4": ("LAM-4", "Logging and Monitoring", "Notification for IdP group mapping changes", 1),
|
||||||
|
"4.5": ("LAM-5", "Logging and Monitoring", "Notification for IAM group changes", 1),
|
||||||
|
"4.6": ("LAM-6", "Logging and Monitoring", "Notification for IAM policy changes", 1),
|
||||||
|
"4.7": ("LAM-7", "Logging and Monitoring", "Notification for user changes", 1),
|
||||||
|
"4.8": ("LAM-8", "Logging and Monitoring", "Notification for VCN changes", 1),
|
||||||
|
"4.9": ("LAM-9", "Logging and Monitoring", "Notification for route table changes", 1),
|
||||||
|
"4.10": ("LAM-10", "Logging and Monitoring", "Notification for security list changes", 1),
|
||||||
|
"4.11": ("LAM-11", "Logging and Monitoring", "Notification for NSG changes", 1),
|
||||||
|
"4.12": ("LAM-12", "Logging and Monitoring", "Notification for network gateway changes", 1),
|
||||||
|
"4.13": ("LAM-13", "Logging and Monitoring", "VCN flow logging enabled for all subnets", 2),
|
||||||
|
"4.14": ("LAM-14", "Logging and Monitoring", "Cloud Guard enabled in root compartment", 1),
|
||||||
|
"4.15": ("LAM-15", "Logging and Monitoring", "Notification for Cloud Guard problems", 2),
|
||||||
|
"4.16": ("LAM-16", "Logging and Monitoring", "CMK rotated at least annually", 1),
|
||||||
|
"4.17": ("LAM-17", "Logging and Monitoring", "Write level Object Storage logging enabled", 2),
|
||||||
|
"4.18": ("LAM-18", "Logging and Monitoring", "Notification for Local OCI User Authentication", 1),
|
||||||
|
"5.1.1": ("STO-1-1", "Storage - Object Storage", "No Object Storage buckets publicly visible", 1),
|
||||||
|
"5.1.2": ("STO-1-2", "Storage - Object Storage", "Object Storage encrypted with CMK", 2),
|
||||||
|
"5.1.3": ("STO-1-3", "Storage - Object Storage", "Versioning enabled for Object Storage", 2),
|
||||||
|
"5.2.1": ("STO-2-1", "Storage - Block Volumes", "Block Volumes encrypted with CMK", 2),
|
||||||
|
"5.2.2": ("STO-2-2", "Storage - Block Volumes", "Boot Volumes encrypted with CMK", 2),
|
||||||
|
"5.3.1": ("STO-3-1", "Storage - File Storage Service", "File Storage encrypted with CMK", 2),
|
||||||
|
"6.1": ("AM-1", "Asset Management", "At least one compartment exists", 1),
|
||||||
|
"6.2": ("AM-2", "Asset Management", "No resources in root compartment", 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Tool Definitions
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
TOOLS = [
|
||||||
|
Tool(name="cis_list_configs",
|
||||||
|
description="Lista as configurações OCI disponíveis. Não requer parâmetros.",
|
||||||
|
inputSchema={"type": "object", "properties": {}, "required": []}),
|
||||||
|
Tool(name="cis_list_checks",
|
||||||
|
description="Lista todos os 48 checks CIS disponíveis com ID, título, seção e nível. Estático, sem conexão OCI.",
|
||||||
|
inputSchema={"type": "object", "properties": {
|
||||||
|
"section": {"type": "string", "enum": ["iam", "networking", "compute", "logging_monitoring", "storage", "asset_management"], "description": "Filtrar por seção"},
|
||||||
|
"level": {"type": "integer", "enum": [1, 2], "description": "Filtrar por nível CIS"},
|
||||||
|
}, "required": []}),
|
||||||
|
Tool(name="cis_scan_iam",
|
||||||
|
description="Coleta dados IAM e executa checks CIS 1.1-1.17: políticas, usuários, MFA, API keys, passwords, dynamic groups. Rápido (~30s).",
|
||||||
|
inputSchema={"type": "object", "properties": {
|
||||||
|
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||||
|
}, "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"},
|
||||||
|
}, "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"},
|
||||||
|
}, "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"},
|
||||||
|
}, "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"},
|
||||||
|
}, "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"},
|
||||||
|
}, "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.",
|
||||||
|
inputSchema={"type": "object", "properties": {
|
||||||
|
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||||
|
"check_id": {"type": "string", "description": "ID do check CIS (ex: '1.7')"},
|
||||||
|
}, "required": ["config_id", "check_id"]}),
|
||||||
|
Tool(name="cis_get_remediation",
|
||||||
|
description="Orientação de remediação para um check CIS: descrição, passos, link docs Oracle. Estático, sem conexão OCI.",
|
||||||
|
inputSchema={"type": "object", "properties": {
|
||||||
|
"check_id": {"type": "string", "description": "ID do check CIS (ex: '1.7')"},
|
||||||
|
}, "required": ["check_id"]}),
|
||||||
|
Tool(name="cis_get_scan_status",
|
||||||
|
description="Status da sessão: quais seções já foram coletadas/analisadas, TTL restante.",
|
||||||
|
inputSchema={"type": "object", "properties": {
|
||||||
|
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||||
|
}, "required": ["config_id"]}),
|
||||||
|
Tool(name="cis_invalidate_cache",
|
||||||
|
description="Limpa cache de sessão. Próximo scan coletará dados novamente.",
|
||||||
|
inputSchema={"type": "object", "properties": {
|
||||||
|
"config_id": {"type": "string", "description": "ID da config OCI"},
|
||||||
|
}, "required": ["config_id"]}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# Handlers
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _handle_cis_list_configs(args):
|
||||||
|
configs = _list_available_configs()
|
||||||
|
return {"configs": configs, "total": len(configs)}
|
||||||
|
|
||||||
|
def _handle_cis_list_checks(args):
|
||||||
|
sf = args.get("section"); lf = args.get("level")
|
||||||
|
results = []
|
||||||
|
for rec_id, (cid, sec, title, lvl) in CHECKS_CATALOG.items():
|
||||||
|
if sf and _check_id_to_section(rec_id) != sf: continue
|
||||||
|
if lf and lvl != lf: continue
|
||||||
|
results.append({"check_id": rec_id, "id": cid, "section": sec, "title": title, "level": lvl})
|
||||||
|
return {"checks": results, "total": len(results)}
|
||||||
|
|
||||||
|
def _handle_scan_section(args, section: str):
|
||||||
|
config_id = args["config_id"]
|
||||||
|
session = _get_or_create_checker(config_id)
|
||||||
|
_analyze_section(session, section)
|
||||||
|
return _section_summary(session["checker"], section)
|
||||||
|
|
||||||
|
def _handle_cis_get_check(args):
|
||||||
|
config_id = args["config_id"]; check_id = args["check_id"]
|
||||||
|
section = _check_id_to_section(check_id)
|
||||||
|
if section == "unknown":
|
||||||
|
return {"error": f"Check '{check_id}' não encontrado. Use cis_list_checks."}
|
||||||
|
session = _get_or_create_checker(config_id)
|
||||||
|
_analyze_section(session, section)
|
||||||
|
benchmark = _get_benchmark(session["checker"])
|
||||||
|
if check_id not in benchmark:
|
||||||
|
return {"error": f"Check '{check_id}' não encontrado."}
|
||||||
|
check = benchmark[check_id]
|
||||||
|
report_data = session["checker"].cis_report_data.get(check_id, {})
|
||||||
|
return {
|
||||||
|
"check_id": check_id, "id": check["id"], "title": check["Title"], "section": check["section"],
|
||||||
|
"status": "PASS" if check["Status"] is True else ("FAIL" if check["Status"] is False else "N/A"),
|
||||||
|
"level": check.get("Level", "N/A"),
|
||||||
|
"total_resources": len(check.get("Total", [])), "findings_count": len(check.get("Findings", [])),
|
||||||
|
"findings": _serialize_oci_obj(check.get("Findings", [])[:30]),
|
||||||
|
"description": report_data.get("Description", ""),
|
||||||
|
"remediation_guidance": report_data.get("Remediation", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _handle_cis_get_remediation(args):
|
||||||
|
cid = args["check_id"]
|
||||||
|
if cid not in REMEDIATIONS:
|
||||||
|
return {"error": f"Check '{cid}' não encontrado."}
|
||||||
|
return {"check_id": cid, **REMEDIATIONS[cid]}
|
||||||
|
|
||||||
|
def _handle_cis_get_scan_status(args):
|
||||||
|
config_id = args["config_id"]
|
||||||
|
if config_id not in _sessions:
|
||||||
|
return {"config_id": config_id, "status": "not_initialized", "sections_collected": [], "sections_analyzed": []}
|
||||||
|
s = _sessions[config_id]
|
||||||
|
elapsed = time.time() - s["created_at"]
|
||||||
|
return {
|
||||||
|
"config_id": config_id, "status": "active",
|
||||||
|
"base_collected": s["base_collected"],
|
||||||
|
"sections_collected": list(s["sections_collected"]),
|
||||||
|
"sections_analyzed": list(s["sections_analyzed"]),
|
||||||
|
"session_age_seconds": round(elapsed),
|
||||||
|
"ttl_remaining_seconds": round(max(0, SESSION_TTL - elapsed)),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _handle_cis_invalidate_cache(args):
|
||||||
|
config_id = args["config_id"]
|
||||||
|
if config_id in _sessions:
|
||||||
|
del _sessions[config_id]
|
||||||
|
return {"status": "cleared", "config_id": config_id}
|
||||||
|
return {"status": "not_found", "config_id": config_id}
|
||||||
|
|
||||||
|
|
||||||
|
TOOL_HANDLERS = {
|
||||||
|
"cis_list_configs": _handle_cis_list_configs,
|
||||||
|
"cis_list_checks": _handle_cis_list_checks,
|
||||||
|
"cis_scan_iam": lambda a: _handle_scan_section(a, "iam"),
|
||||||
|
"cis_scan_networking": lambda a: _handle_scan_section(a, "networking"),
|
||||||
|
"cis_scan_compute": lambda a: _handle_scan_section(a, "compute"),
|
||||||
|
"cis_scan_logging_monitoring": lambda a: _handle_scan_section(a, "logging_monitoring"),
|
||||||
|
"cis_scan_storage": lambda a: _handle_scan_section(a, "storage"),
|
||||||
|
"cis_scan_asset_management": lambda a: _handle_scan_section(a, "asset_management"),
|
||||||
|
"cis_get_check": _handle_cis_get_check,
|
||||||
|
"cis_get_remediation": _handle_cis_get_remediation,
|
||||||
|
"cis_get_scan_status": _handle_cis_get_scan_status,
|
||||||
|
"cis_invalidate_cache": _handle_cis_invalidate_cache,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
# MCP Interface
|
||||||
|
# ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def list_tools() -> list[Tool]:
|
||||||
|
return TOOLS
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
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 loop.run_in_executor(None, partial(handler, arguments))
|
||||||
|
return [TextContent(type="text", text=json.dumps(result, default=str, ensure_ascii=False))]
|
||||||
|
except Exception as e:
|
||||||
|
return [TextContent(type="text", text=json.dumps({"error": str(e), "traceback": traceback.format_exc()}))]
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
|
await server.run(read_stream, write_stream, server.create_initialization_options())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -6,3 +6,4 @@ python-dotenv==1.0.1
|
|||||||
oci==2.133.0
|
oci==2.133.0
|
||||||
oracledb==2.4.1
|
oracledb==2.4.1
|
||||||
mcp>=1.0.0
|
mcp>=1.0.0
|
||||||
|
requests>=2.31.0
|
||||||
|
|||||||
@@ -345,21 +345,28 @@ async function sChat(){const el=document.getElementById('chi');const m=el.value.
|
|||||||
if(!S.chatModel){S.msgs.push({r:'assistant',c:'⚠️ Selecione um modelo antes de enviar uma mensagem.'});R();return}
|
if(!S.chatModel){S.msgs.push({r:'assistant',c:'⚠️ Selecione um modelo antes de enviar uma mensagem.'});R();return}
|
||||||
el.value='';
|
el.value='';
|
||||||
S.msgs.push({r:'user',c:m});R();scCh();
|
S.msgs.push({r:'user',c:m});R();scCh();
|
||||||
|
// Disable input and show thinking indicator
|
||||||
|
const btn=document.querySelector('.ch-i .btn');
|
||||||
|
el.disabled=true;if(btn){btn.disabled=true;btn.dataset.origText=btn.textContent;btn.innerHTML='<span class="spinner" style="width:14px;height:14px;border-width:2px;margin-right:6px"></span>Pensando...'}
|
||||||
|
S.msgs.push({r:'assistant',c:'⏳ _Pensando..._',thinking:true});R();scCh();
|
||||||
try{const body={message:m,session_id:S.sid,use_tools:S.chatUseTools};
|
try{const body={message:m,session_id:S.sid,use_tools:S.chatUseTools};
|
||||||
if(S.chatModel.startsWith('cfg:')){
|
if(S.chatModel.startsWith('cfg:')){
|
||||||
body.genai_config_id=S.chatModel.slice(4);
|
body.genai_config_id=S.chatModel.slice(4);
|
||||||
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
|
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
|
||||||
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
||||||
else if(S.chatModel){
|
else if(S.chatModel){
|
||||||
if(!S.chatOci){S.msgs.push({r:'assistant',c:'⚠️ Selecione uma credencial OCI para usar o modelo direto.'});R();return}
|
if(!S.chatOci){S.msgs.pop();S.msgs.push({r:'assistant',c:'⚠️ Selecione uma credencial OCI para usar o modelo direto.'});R();el.disabled=false;if(btn){btn.disabled=false;btn.textContent=btn.dataset.origText}return}
|
||||||
body.model_id=S.chatModel;body.oci_config_id=S.chatOci;body.genai_region=S.chatRegion;body.compartment_id=S.chatCompartment;
|
body.model_id=S.chatModel;body.oci_config_id=S.chatOci;body.genai_region=S.chatRegion;body.compartment_id=S.chatCompartment;
|
||||||
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
|
body.temperature=S.chatParams.temperature;body.max_tokens=S.chatParams.max_tokens;body.top_p=S.chatParams.top_p;
|
||||||
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
body.top_k=S.chatParams.top_k;body.frequency_penalty=S.chatParams.frequency_penalty;body.presence_penalty=S.chatParams.presence_penalty}
|
||||||
const d=await $api('/chat',{method:'POST',body});S.sid=d.session_id;
|
const d=await $api('/chat',{method:'POST',body});S.sid=d.session_id;
|
||||||
|
// Remove thinking indicator
|
||||||
|
S.msgs=S.msgs.filter(x=>!x.thinking);
|
||||||
let resp=d.response;
|
let resp=d.response;
|
||||||
if(d.tools_used&&d.tools_used.length){resp+='\n\n🔧 **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
|
if(d.tools_used&&d.tools_used.length){resp+='\n\n🔧 **Tools utilizadas:** '+d.tools_used.map(t=>t.name).join(', ')}
|
||||||
S.msgs.push({r:'assistant',c:resp});R();scCh()}
|
S.msgs.push({r:'assistant',c:resp});R();scCh()}
|
||||||
catch(e){S.msgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}}
|
catch(e){S.msgs=S.msgs.filter(x=>!x.thinking);S.msgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}
|
||||||
|
finally{el.disabled=false;el.focus();if(btn){btn.disabled=false;btn.textContent=btn.dataset.origText||'Enviar →'}}}
|
||||||
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
|
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
|
||||||
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
|
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user