diff --git a/.DS_Store b/.DS_Store index aad892a..b5123ee 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.idea/misc.xml b/.idea/misc.xml index 0d17ccd..175f79d 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,7 @@ + + \ No newline at end of file diff --git a/backend/app.py b/backend/app.py index f24bfcf..065793c 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,10 +1,11 @@ """ -OCI CIS AI Agent - Backend API v2 -FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI integration (Cohere/Meta/xAI/Google), -MCP Server registry, Autonomous DB vector storage, CIS reports, chat agent, audit log. +OCI CIS AI Agent - Backend API v1.1 +FastAPI with JWT auth, TOTP MFA, RBAC, OCI GenAI (exact SDK pattern), +OCI Account Explorer, MCP Server registry with VectorDB tool integration, +Autonomous DB vector storage, CIS reports, chat agent, audit log. """ import os, json, uuid, hashlib, hmac, time, base64, struct, secrets, subprocess -import shutil, asyncio, sqlite3, logging, socket, re, importlib, sys +import shutil, asyncio, sqlite3, logging, socket, re from datetime import datetime, timedelta from pathlib import Path from typing import Optional, List, Dict, Any @@ -30,6 +31,7 @@ OCI_DIR = DATA / "oci_configs" REPORTS = DATA / "reports" MCP_DIR = DATA / "mcp_servers" WALLET_DIR = DATA / "wallets" +VERSION = "1.1" for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]: d.mkdir(parents=True, exist_ok=True) @@ -37,7 +39,7 @@ for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]: logging.basicConfig(level=logging.INFO) log = logging.getLogger("agent") -app = FastAPI(title="OCI CIS AI Agent", version="2.0.0") +app = FastAPI(title="OCI CIS AI Agent", version=VERSION) app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) security = HTTPBearer() @@ -104,16 +106,21 @@ def init_db(): ); CREATE TABLE IF NOT EXISTS genai_configs ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, + name TEXT NOT NULL DEFAULT 'default', oci_config_id TEXT NOT NULL, model_id TEXT NOT NULL, + model_ocid TEXT, compartment_id TEXT NOT NULL, genai_region TEXT NOT NULL, + endpoint TEXT NOT NULL, serving_type TEXT DEFAULT 'ON_DEMAND', - endpoint_id TEXT, - temperature REAL DEFAULT 0.7, - max_tokens INTEGER DEFAULT 2048, - top_p REAL DEFAULT 0.9, - top_k INTEGER DEFAULT -1, + dedicated_endpoint_id TEXT, + temperature REAL DEFAULT 1, + max_tokens INTEGER DEFAULT 6000, + top_p REAL DEFAULT 0.95, + top_k INTEGER DEFAULT 1, + frequency_penalty REAL DEFAULT 0, + presence_penalty REAL DEFAULT 0, is_default INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (oci_config_id) REFERENCES oci_configs(id) @@ -146,6 +153,8 @@ def init_db(): server_type TEXT NOT NULL DEFAULT 'stdio', command TEXT, args TEXT, env_vars TEXT, url TEXT, module_path TEXT, + tools TEXT, + linked_adb_id TEXT, is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); @@ -187,6 +196,12 @@ def _make_token(uid,role,sid): def _enc(v): return base64.b64encode(v.encode()).decode() def _dec(v): return base64.b64decode(v.encode()).decode() +# ── OCI SDK Client Helper ───────────────────────────────────────────────────── +def _get_oci_config(oci_config_id: str) -> dict: + import oci + config_path = str(OCI_DIR / oci_config_id / "config") + return oci.config.from_file(config_path, "DEFAULT") + # ── Auth deps ───────────────────────────────────────────────────────────────── async def current_user(cred: HTTPAuthorizationCredentials = Depends(security)): try: p = pyjwt.decode(cred.credentials, APP_SECRET, algorithms=[JWT_ALG]) @@ -221,13 +236,17 @@ class ChangePwReq(BaseModel): class UserUpdateReq(BaseModel): email: Optional[str] = None; role: Optional[str] = None; is_active: Optional[bool] = None class ChatMsg(BaseModel): - message: str; session_id: Optional[str] = None; model_id: Optional[str] = None; genai_config_id: Optional[str] = None + message: str; session_id: Optional[str] = None; genai_config_id: Optional[str] = None class RunReportReq(BaseModel): config_id: str; mcp_server_id: Optional[str] = None; regions: Optional[List[str]] = None class GenAIConfigReq(BaseModel): - oci_config_id: str; model_id: str; compartment_id: str; genai_region: str - serving_type: str = "ON_DEMAND"; endpoint_id: Optional[str] = None - temperature: float = 0.7; max_tokens: int = 2048; top_p: float = 0.9; top_k: int = -1 + name: str = "default" + oci_config_id: str; model_id: str; model_ocid: Optional[str] = None + compartment_id: str; genai_region: str + endpoint: Optional[str] = None + serving_type: str = "ON_DEMAND"; dedicated_endpoint_id: Optional[str] = None + temperature: float = 1; max_tokens: int = 6000; top_p: float = 0.95 + top_k: int = 1; frequency_penalty: float = 0; presence_penalty: float = 0 is_default: bool = False class ADBVectorReq(BaseModel): config_name: str; dsn: str; username: str; password: str @@ -235,7 +254,9 @@ class ADBVectorReq(BaseModel): class MCPServerReq(BaseModel): name: str; description: Optional[str] = None; server_type: str = "stdio" command: Optional[str] = None; args: Optional[List[str]] = None - env_vars: Optional[Dict[str,str]] = None; url: Optional[str] = None; module_path: Optional[str] = None + env_vars: Optional[Dict[str,str]] = None; url: Optional[str] = None + module_path: Optional[str] = None; tools: Optional[List[str]] = None + linked_adb_id: Optional[str] = None # ── Auth endpoints ──────────────────────────────────────────────────────────── @app.post("/api/auth/login") @@ -314,9 +335,9 @@ async def update_user(uid: str, req: UserUpdateReq, adm=Depends(require("admin") if req.role not in ("admin","user","viewer"): raise HTTPException(400, "Role inválida") sets.append("role=?"); vals.append(req.role) if req.is_active is not None: sets.append("is_active=?"); vals.append(int(req.is_active)) - if sets: vals.append(uid); c_ = db() - with db() as c: - if sets: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals) + if sets: + vals.append(uid) + with db() as c: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals) _audit(adm["id"], adm["username"], "update_user", uid) return {"ok": True} @@ -340,9 +361,11 @@ async def save_oci( kp = cdir / "oci_api_key.pem" kp.write_bytes(await private_key.read()); kp.chmod(0o600) if public_key: (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read()) - (cdir / "config").write_text( + cfg_file = cdir / "config" + cfg_file.write_text( f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n" f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n") + cfg_file.chmod(0o600) with db() as c: c.execute("INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path,compartment_id) VALUES (?,?,?,?,?,?,?,?,?)", (cid, u["id"], tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, str(kp), compartment_id or None)) @@ -375,11 +398,98 @@ async def test_oci(cid: str, u=Depends(require("admin","user"))): r = subprocess.run(["oci","iam","region","list","--config-file",str(cp),"--output","json"], capture_output=True, text=True, timeout=30) if r.returncode == 0: return {"status":"success","message":"Conexão OK","data":json.loads(r.stdout)} return {"status":"error","message":r.stderr[:500]} - except FileNotFoundError: return {"status":"error","message":"OCI CLI não instalado. Instale via admin panel."} + except FileNotFoundError: return {"status":"error","message":"OCI CLI não disponível no container."} except subprocess.TimeoutExpired: return {"status":"error","message":"Timeout na conexão"} except Exception as e: return {"status":"error","message":str(e)} -# ── OCI GenAI Config ────────────────────────────────────────────────────────── +# ── OCI Account Explorer ────────────────────────────────────────────────────── +@app.get("/api/oci/explore/{cid}/compartments") +async def explore_compartments(cid: str, u=Depends(current_user)): + try: + import oci + config = _get_oci_config(cid) + identity = oci.identity.IdentityClient(config) + with db() as c: + cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone() + root = cfg["compartment_id"] or cfg["tenancy_ocid"] + comps = identity.list_compartments(root, compartment_id_in_subtree=True).data + return [{"id":cp.id,"name":cp.name,"lifecycle_state":cp.lifecycle_state,"description":cp.description or ""} for cp in comps if cp.lifecycle_state == "ACTIVE"] + except Exception as e: + return {"error": str(e)[:500]} + +@app.get("/api/oci/explore/{cid}/regions") +async def explore_regions(cid: str, u=Depends(current_user)): + try: + import oci + config = _get_oci_config(cid) + identity = oci.identity.IdentityClient(config) + with db() as c: + cfg = c.execute("SELECT tenancy_ocid FROM oci_configs WHERE id=?", (cid,)).fetchone() + regions = identity.list_region_subscriptions(cfg["tenancy_ocid"]).data + return [{"name":r.region_name,"key":r.region_key,"status":r.status,"is_home":r.is_home_region} for r in regions] + except Exception as e: + return {"error": str(e)[:500]} + +@app.get("/api/oci/explore/{cid}/vcns") +async def explore_vcns(cid: str, compartment_id: str = Query(None), u=Depends(current_user)): + try: + import oci + config = _get_oci_config(cid) + vn = oci.core.VirtualNetworkClient(config) + with db() as c: + cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone() + comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"] + vcns = vn.list_vcns(comp).data + return [{"id":v.id,"display_name":v.display_name,"cidr_blocks":v.cidr_blocks,"lifecycle_state":v.lifecycle_state} for v in vcns] + except Exception as e: + return {"error": str(e)[:500]} + +@app.get("/api/oci/explore/{cid}/instances") +async def explore_instances(cid: str, compartment_id: str = Query(None), u=Depends(current_user)): + try: + import oci + config = _get_oci_config(cid) + compute = oci.core.ComputeClient(config) + with db() as c: + cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone() + comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"] + insts = compute.list_instances(comp).data + return [{"id":i.id,"display_name":i.display_name,"shape":i.shape,"lifecycle_state":i.lifecycle_state,"region":i.region,"time_created":str(i.time_created)} for i in insts] + except Exception as e: + return {"error": str(e)[:500]} + +@app.get("/api/oci/explore/{cid}/databases") +async def explore_databases(cid: str, compartment_id: str = Query(None), u=Depends(current_user)): + try: + import oci + config = _get_oci_config(cid) + db_client = oci.database.DatabaseClient(config) + with db() as c: + cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone() + comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"] + adbs = db_client.list_autonomous_databases(comp).data + return [{"id":a.id,"display_name":a.display_name,"db_name":a.db_name,"lifecycle_state":a.lifecycle_state, + "cpu_core_count":a.cpu_core_count,"data_storage_size_in_tbs":a.data_storage_size_in_tbs, + "is_free_tier":a.is_free_tier} for a in adbs] + except Exception as e: + return {"error": str(e)[:500]} + +@app.get("/api/oci/explore/{cid}/buckets") +async def explore_buckets(cid: str, compartment_id: str = Query(None), u=Depends(current_user)): + try: + import oci + config = _get_oci_config(cid) + os_client = oci.object_storage.ObjectStorageClient(config) + namespace = os_client.get_namespace().data + with db() as c: + cfg = c.execute("SELECT tenancy_ocid,compartment_id FROM oci_configs WHERE id=?", (cid,)).fetchone() + comp = compartment_id or cfg["compartment_id"] or cfg["tenancy_ocid"] + buckets = os_client.list_buckets(namespace, comp).data + return [{"name":b.name,"namespace":b.namespace,"time_created":str(b.time_created)} for b in buckets] + except Exception as e: + return {"error": str(e)[:500]} + +# ── OCI GenAI Config & Chat ─────────────────────────────────────────────────── @app.get("/api/genai/models") async def list_genai_models(u=Depends(current_user)): return {"models": GENAI_MODELS, "regions": GENAI_REGIONS} @@ -387,15 +497,20 @@ async def list_genai_models(u=Depends(current_user)): @app.post("/api/genai/config") async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))): gid = str(uuid.uuid4()) + ep = req.endpoint or f"https://inference.generativeai.{req.genai_region}.oci.oraclecloud.com" with db() as c: if req.is_default: c.execute("UPDATE genai_configs SET is_default=0 WHERE user_id=?", (u["id"],)) c.execute( - "INSERT INTO genai_configs (id,user_id,oci_config_id,model_id,compartment_id,genai_region,serving_type,endpoint_id,temperature,max_tokens,top_p,top_k,is_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", - (gid, u["id"], req.oci_config_id, req.model_id, req.compartment_id, req.genai_region, - req.serving_type, req.endpoint_id, req.temperature, req.max_tokens, req.top_p, req.top_k, int(req.is_default))) + """INSERT INTO genai_configs (id,user_id,name,oci_config_id,model_id,model_ocid,compartment_id, + genai_region,endpoint,serving_type,dedicated_endpoint_id,temperature,max_tokens,top_p,top_k, + frequency_penalty,presence_penalty,is_default) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", + (gid, u["id"], req.name, req.oci_config_id, req.model_id, req.model_ocid, + req.compartment_id, req.genai_region, ep, req.serving_type, req.dedicated_endpoint_id, + req.temperature, req.max_tokens, req.top_p, req.top_k, + req.frequency_penalty, req.presence_penalty, int(req.is_default))) _audit(u["id"], u["username"], "save_genai_config", gid, req.model_id) - return {"id": gid, "model_id": req.model_id} + return {"id": gid, "model_id": req.model_id, "endpoint": ep} @app.get("/api/genai/configs") async def list_genai(u=Depends(current_user)): @@ -411,79 +526,123 @@ async def del_genai(gid: str, u=Depends(require("admin","user"))): @app.post("/api/genai/test/{gid}") async def test_genai(gid: str, u=Depends(require("admin","user"))): - """Test GenAI connection by sending a simple prompt""" with db() as c: - gc = c.execute("SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.id=?",(gid,)).fetchone() + gc = c.execute("SELECT * FROM genai_configs WHERE id=?",(gid,)).fetchone() if not gc: raise HTTPException(404) try: - resp = await _call_genai(dict(gc), "Say 'connection successful' in one sentence.") - return {"status":"success","message":"GenAI OK","response":resp[:200]} + resp = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.") + return {"status":"success","message":"GenAI OK","response":resp[:300]} except Exception as e: return {"status":"error","message":str(e)[:500]} -async def _call_genai(gc: dict, message: str, history: list = None) -> str: - """Call OCI Generative AI chat endpoint using OCI SDK""" - try: - import oci - except ImportError: - return "❌ OCI SDK não instalado. Execute: pip install oci" +def _call_genai(gc: dict, message: str, history: list = None) -> str: + """ + Call OCI Generative AI using the exact SDK pattern from chat_demo.py. + Uses oci.generative_ai_inference with proper Message/TextContent objects. + """ + import oci - config_path = str(Path(gc["key_file_path"]).parent / "config") - try: - config = oci.config.from_file(config_path, "DEFAULT") - except Exception: - config = { - "user": gc["user_ocid"], "tenancy": gc["tenancy_ocid"], - "fingerprint": gc["fingerprint"], "key_file": gc["key_file_path"], - "region": gc["genai_region"] - } + # Load OCI config from stored credentials (same as ~/.oci/config) + config_path = str(OCI_DIR / gc["oci_config_id"] / "config") + config = oci.config.from_file(config_path, "DEFAULT") - endpoint = f"https://inference.generativeai.{gc['genai_region']}.oci.oraclecloud.com" - client = oci.generative_ai_inference.GenerativeAiInferenceClient(config, service_endpoint=endpoint) + # Service endpoint - built from region + endpoint = gc["endpoint"] + # Create inference client with retry strategy and timeout + generative_ai_inference_client = oci.generative_ai_inference.GenerativeAiInferenceClient( + config=config, + service_endpoint=endpoint, + retry_strategy=oci.retry.NoneRetryStrategy(), + timeout=(10, 240) + ) + + # Build ChatDetails + chat_detail = oci.generative_ai_inference.models.ChatDetails() + + # Determine API format from model catalog model_info = GENAI_MODELS.get(gc["model_id"], {}) api_format = model_info.get("api_format", "GENERIC") - if gc["serving_type"] == "DEDICATED" and gc.get("endpoint_id"): - serving_mode = oci.generative_ai_inference.models.DedicatedServingMode(endpoint_id=gc["endpoint_id"]) - else: - serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=gc["model_id"]) - if api_format == "COHERE": - chat_request = oci.generative_ai_inference.models.CohereChatRequest( - message=message, max_tokens=gc.get("max_tokens", 2048), - temperature=gc.get("temperature", 0.7), top_p=gc.get("top_p", 0.9), - api_format="COHERE" - ) + # ── Cohere models (CohereChatRequest) ── + chat_request = oci.generative_ai_inference.models.CohereChatRequest() + chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_COHERE + chat_request.message = message + chat_request.max_tokens = int(gc.get("max_tokens", 6000)) + chat_request.temperature = float(gc.get("temperature", 1)) + chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0)) + chat_request.presence_penalty = float(gc.get("presence_penalty", 0)) + chat_request.top_p = float(gc.get("top_p", 0.95)) + chat_request.top_k = int(gc.get("top_k", 1)) + if history: + chat_history = [] + for h in history: + entry = oci.generative_ai_inference.models.CohereMessage() + entry.role = "USER" if h["role"] == "user" else "CHATBOT" + entry.message = h["content"] + chat_history.append(entry) + chat_request.chat_history = chat_history else: - msgs = [] + # ── Generic format (Meta Llama, Google, xAI) - exact chat_demo.py pattern ── + chat_request = oci.generative_ai_inference.models.GenericChatRequest() + chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC + chat_request.max_tokens = int(gc.get("max_tokens", 6000)) + chat_request.temperature = float(gc.get("temperature", 1)) + chat_request.frequency_penalty = float(gc.get("frequency_penalty", 0)) + chat_request.presence_penalty = float(gc.get("presence_penalty", 0)) + chat_request.top_p = float(gc.get("top_p", 0.95)) + chat_request.top_k = int(gc.get("top_k", 1)) + + messages = [] if history: for h in history: - role = "USER" if h["role"] == "user" else "ASSISTANT" - msgs.append(oci.generative_ai_inference.models.GenericChatMessage( - role=role, content=[oci.generative_ai_inference.models.TextContent(text=h["content"])] - )) - msgs.append(oci.generative_ai_inference.models.GenericChatMessage( - role="USER", content=[oci.generative_ai_inference.models.TextContent(text=message)] - )) - chat_request = oci.generative_ai_inference.models.GenericChatRequest( - messages=msgs, max_tokens=gc.get("max_tokens", 2048), - temperature=gc.get("temperature", 0.7), top_p=gc.get("top_p", 0.9), - api_format="GENERIC" + content = oci.generative_ai_inference.models.TextContent() + content.text = h["content"] + msg = oci.generative_ai_inference.models.Message() + msg.role = "USER" if h["role"] == "user" else "ASSISTANT" + msg.content = [content] + messages.append(msg) + + # Current user message + content = oci.generative_ai_inference.models.TextContent() + content.text = message + user_message = oci.generative_ai_inference.models.Message() + user_message.role = "USER" + user_message.content = [content] + messages.append(user_message) + + chat_request.messages = messages + + # Serving mode - supports model_id or model_ocid + model_ref = gc.get("model_ocid") or gc["model_id"] + if gc.get("serving_type") == "DEDICATED" and gc.get("dedicated_endpoint_id"): + chat_detail.serving_mode = oci.generative_ai_inference.models.DedicatedServingMode( + endpoint_id=gc["dedicated_endpoint_id"] + ) + else: + chat_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode( + model_id=model_ref ) - chat_detail = oci.generative_ai_inference.models.ChatDetails( - compartment_id=gc["compartment_id"], serving_mode=serving_mode, chat_request=chat_request - ) - response = client.chat(chat_detail) - chat_response = response.data.chat_response + chat_detail.chat_request = chat_request + chat_detail.compartment_id = gc["compartment_id"] + # Execute + chat_response = generative_ai_inference_client.chat(chat_detail) + + # Extract text from response + resp = chat_response.data.chat_response if api_format == "COHERE": - return chat_response.text if hasattr(chat_response, 'text') else str(chat_response) + return resp.text if hasattr(resp, 'text') else str(resp) else: - if hasattr(chat_response, 'choices') and chat_response.choices: - return chat_response.choices[0].message.content[0].text if chat_response.choices[0].message.content else "" - return str(chat_response) + if hasattr(resp, 'choices') and resp.choices: + choice = resp.choices[0] + if hasattr(choice, 'message') and choice.message and hasattr(choice.message, 'content'): + contents = choice.message.content + if contents and len(contents) > 0: + return contents[0].text + return str(resp) # ── MCP Servers ─────────────────────────────────────────────────────────────── @app.post("/api/mcp/servers") @@ -491,10 +650,11 @@ async def register_mcp(req: MCPServerReq, u=Depends(require("admin","user"))): mid = str(uuid.uuid4()) with db() as c: c.execute( - "INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path) VALUES (?,?,?,?,?,?,?,?,?,?)", + "INSERT INTO mcp_servers (id,user_id,name,description,server_type,command,args,env_vars,url,module_path,tools,linked_adb_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)", (mid, u["id"], req.name, req.description, req.server_type, req.command, json.dumps(req.args) if req.args else None, - json.dumps(req.env_vars) if req.env_vars else None, req.url, req.module_path)) + json.dumps(req.env_vars) if req.env_vars else None, req.url, req.module_path, + json.dumps(req.tools) if req.tools else None, req.linked_adb_id)) _audit(u["id"], u["username"], "register_mcp", mid, req.name) return {"id": mid, "name": req.name, "server_type": req.server_type} @@ -506,8 +666,10 @@ async def list_mcp(u=Depends(current_user)): res = [] for r in rows: d = dict(r) - if d.get("args"): d["args"] = json.loads(d["args"]) - if d.get("env_vars"): d["env_vars"] = json.loads(d["env_vars"]) + for k in ("args","env_vars","tools"): + if d.get(k): + try: d[k] = json.loads(d[k]) + except: pass res.append(d) return res @@ -526,14 +688,17 @@ async def toggle_mcp(mid: str, u=Depends(require("admin","user"))): @app.post("/api/mcp/servers/{mid}/upload") async def upload_mcp_file(mid: str, file: UploadFile = File(...), u=Depends(require("admin","user"))): - """Upload a Python MCP server script""" sdir = MCP_DIR / mid; sdir.mkdir(parents=True, exist_ok=True) fp = sdir / file.filename fp.write_bytes(await file.read()) - with db() as c: - c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid)) + with db() as c: c.execute("UPDATE mcp_servers SET module_path=? WHERE id=?", (str(fp), mid)) return {"ok": True, "path": str(fp)} +@app.put("/api/mcp/servers/{mid}/link-adb") +async def link_mcp_adb(mid: str, adb_id: str = Query(...), u=Depends(require("admin","user"))): + with db() as c: c.execute("UPDATE mcp_servers SET linked_adb_id=? WHERE id=?", (adb_id, mid)) + return {"ok": True, "mcp_id": mid, "linked_adb_id": adb_id} + # ── ADB Vector DB Config ───────────────────────────────────────────────────── @app.post("/api/adb/config") async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))): @@ -580,7 +745,7 @@ async def test_adb(vid: str, u=Depends(require("admin","user"))): cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close() return {"status":"success","message":"Conexão Autonomous DB OK!"} except ImportError: - return {"status":"error","message":"python-oracledb não instalado. Execute: pip install oracledb"} + return {"status":"error","message":"python-oracledb não disponível no container."} except Exception as e: return {"status":"error","message":str(e)[:500]} @@ -614,7 +779,13 @@ async def _exec_report(rid, cfg, regions, mcp_server_id): if mcp_server_id: with db() as c: mcp = c.execute("SELECT * FROM mcp_servers WHERE id=?",(mcp_server_id,)).fetchone() - if mcp and mcp["module_path"]: cmd += ["--mcp-module", mcp["module_path"]] + if mcp: + if mcp["module_path"]: cmd += ["--mcp-module", mcp["module_path"]] + if mcp.get("linked_adb_id"): + adb_cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?",(mcp["linked_adb_id"],)).fetchone() + if adb_cfg: + cmd += ["--adb-dsn", adb_cfg["dsn"], "--adb-user", adb_cfg["username"]] + if adb_cfg.get("wallet_dir"): cmd += ["--adb-wallet", adb_cfg["wallet_dir"]] proc = await asyncio.create_subprocess_exec(*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) stdout, stderr = await proc.communicate() jp = rdir / "report.json"; hp = rdir / "report.html" @@ -668,36 +839,30 @@ async def chat(msg: ChatMsg, u=Depends(current_user)): sid = msg.session_id or str(uuid.uuid4()) with db() as c: c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)", - (str(uuid.uuid4()), sid, u["id"], "user", msg.message, msg.model_id)) + (str(uuid.uuid4()), sid, u["id"], "user", msg.message, None)) genai_cfg = None if msg.genai_config_id: with db() as c: - genai_cfg = c.execute( - "SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.id=?", - (msg.genai_config_id,)).fetchone() - elif msg.model_id: - with db() as c: - genai_cfg = c.execute( - "SELECT g.*,o.key_file_path,o.tenancy_ocid,o.user_ocid,o.fingerprint FROM genai_configs g JOIN oci_configs o ON g.oci_config_id=o.id WHERE g.user_id=? AND g.model_id=? LIMIT 1", - (u["id"], msg.model_id)).fetchone() + genai_cfg = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone() if genai_cfg: try: history = [] with db() as c: - prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? ORDER BY created_at ASC", (sid,)).fetchall() + prev = c.execute("SELECT role,content FROM chat_messages WHERE session_id=? AND role IN ('user','assistant') ORDER BY created_at ASC", (sid,)).fetchall() history = [{"role":r["role"],"content":r["content"]} for r in prev] - resp = await _call_genai(dict(genai_cfg), msg.message, history[:-1] if history else None) + resp = _call_genai(dict(genai_cfg), msg.message, history[:-1] if len(history) > 1 else None) except Exception as e: - resp = f"❌ Erro GenAI: {str(e)[:300]}" + resp = f"❌ Erro GenAI: {str(e)[:400]}" else: resp = _agent_respond(msg.message, u) + mid = genai_cfg["model_id"] if genai_cfg else None with db() as c: c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content,model_id) VALUES (?,?,?,?,?,?)", - (str(uuid.uuid4()), sid, u["id"], "assistant", resp, msg.model_id)) - return {"session_id": sid, "response": resp, "model_id": msg.model_id} + (str(uuid.uuid4()), sid, u["id"], "assistant", resp, mid)) + return {"session_id": sid, "response": resp, "model_id": mid} def _agent_respond(msg, user): m = msg.lower().strip() @@ -709,7 +874,7 @@ def _agent_respond(msg, user): nu=c.execute("SELECT COUNT(*) n FROM users WHERE is_active=1").fetchone()["n"] nm=c.execute("SELECT COUNT(*) n FROM mcp_servers WHERE is_active=1").fetchone()["n"] ng=c.execute("SELECT COUNT(*) n FROM genai_configs").fetchone()["n"] - return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online") + return (f"📊 **Status do Sistema**\n\n• OCI CLI: {cli}\n• Configs OCI: {nc}\n• GenAI Models: {ng}\n• MCP Servers: {nm}\n• Relatórios: {nr}\n• Usuários ativos: {nu}\n• Servidor: ✅ Online\n• Versão: v{VERSION}") if any(k in m for k in ["ajuda","help","comandos"]): return ("🤖 **Comandos disponíveis:**\n\n• `status` — Status do sistema\n• `listar configs` — Configurações OCI\n• `verificar cis` — Checks CIS 3.0\n• `modelos` — Modelos GenAI disponíveis\n• `ajuda` — Esta mensagem\n\n💡 Configure um modelo GenAI para chat com IA.") if any(k in m for k in ["modelo","modelos","genai"]): @@ -720,7 +885,7 @@ def _agent_respond(msg, user): return "\n".join(lines) if any(k in m for k in ["cis","benchmark","checks","verificar"]): return ("🔒 **CIS OCI Foundations Benchmark 3.0**\n\n54 controles em 8 domínios:\n• IAM: 17 controles\n• Networking: 8 controles\n• Compute: 3 controles\n• Logging & Monitoring: 18 controles\n• Storage: 6 controles\n• Asset Management: 2 controles\n\nConfigure OCI e execute um relatório na aba **Report**.") - return ("Sou o **OCI CIS AI Agent**. Sem modelo GenAI configurado, uso respostas locais.\n\n" + return ("Sou o **OCI CIS AI Agent v" + VERSION + "**. Sem modelo GenAI configurado, uso respostas locais.\n\n" "Para chat com IA:\n1. Configure **OCI Credentials**\n2. Configure **GenAI** com modelo e região\n3. Selecione o modelo no chat\n\nDigite **ajuda** para ver os comandos.") @app.delete("/api/chat/{sid}") @@ -758,12 +923,12 @@ async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))): # ── Health ──────────────────────────────────────────────────────────────────── @app.get("/api/health") async def health(): - return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":"2.0.0"} + return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":VERSION} @app.on_event("startup") async def startup(): init_db() - log.info("OCI CIS AI Agent v2 API started") + log.info(f"OCI CIS AI Agent v{VERSION} API started") if __name__ == "__main__": import uvicorn diff --git a/docker-compose.yml b/docker-compose.yml index d71399f..4c06497 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,6 +9,7 @@ services: - APP_SECRET=${APP_SECRET:-change-me-in-production-use-a-long-random-string} - JWT_EXPIRY_HOURS=${JWT_EXPIRY_HOURS:-12} - DATA_DIR=/data + - OCI_CLI_SUPPRESS_FILE_PERMISSIONS_WARNING=True volumes: - agent-data:/data ports: diff --git a/frontend/index.html b/frontend/index.html index 9913888..43b375b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -14,7 +14,6 @@ *{margin:0;padding:0;box-sizing:border-box} body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100vh} .app{display:flex;min-height:100vh} -/* ── Sidebar ── */ .sb{width:250px;background:var(--bg1);border-right:1px solid var(--bd);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100} .sb-h{padding:1.15rem 1rem;border-bottom:1px solid var(--bd);background:linear-gradient(135deg,#c74634 0%,#a53727 100%)} .sb-h h1{font-size:.88rem;font-weight:700;color:#fff;font-family:var(--fm);letter-spacing:-.02em;display:flex;align-items:center;gap:.4rem} @@ -28,66 +27,59 @@ body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100v .ui{display:flex;align-items:center;gap:.5rem} .ua{width:28px;height:28px;background:var(--ac);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.7rem;color:#fff} .un{font-weight:600;color:var(--t1);font-size:.78rem}.ur{color:var(--tm);font-size:.63rem;font-family:var(--fm)} -/* ── Main ── */ .mc{flex:1;margin-left:250px;min-height:100vh} .tb{height:50px;background:var(--bg1);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.15rem;position:sticky;top:0;z-index:50} .tb-t{font-weight:600;font-size:.88rem;color:var(--t1)}.tb-a{display:flex;align-items:center;gap:.5rem} .pc{padding:1.15rem} .cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.15rem;margin-bottom:.65rem;box-shadow:var(--sh)} .ct{font-weight:600;font-size:.88rem;margin-bottom:.65rem;display:flex;align-items:center;gap:.35rem;color:var(--t1)} -/* ── Buttons ── */ .btn{display:inline-flex;align-items:center;gap:.35rem;padding:.42rem .8rem;border-radius:var(--r);font-size:.76rem;font-weight:600;cursor:pointer;border:1px solid transparent;transition:all .15s;font-family:var(--fs)} .bp{background:var(--ac);color:#fff;border-color:var(--ac)}.bp:hover{background:var(--ah)} .bs{background:var(--bg1);color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--tm);color:var(--t1)} .bd{background:var(--rdl);color:var(--rd);border-color:rgba(199,70,52,.2)}.bd:hover{background:var(--rd);color:#fff} .bsm{padding:.28rem .55rem;font-size:.7rem} .btn:disabled{opacity:.5;cursor:not-allowed} -/* ── Form ── */ .ig{margin-bottom:.65rem}.ig label{display:block;font-size:.76rem;font-weight:600;color:var(--t2);margin-bottom:.2rem} .ig .ht{font-size:.65rem;color:var(--tm);margin-bottom:.15rem} input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{width:100%;padding:.45rem .6rem;background:var(--bg1);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);font-size:.78rem;font-family:var(--fm);transition:border .15s} input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac);box-shadow:0 0 0 2px rgba(199,70,52,.12)} input[type=file]{font-family:var(--fm);font-size:.73rem;color:var(--t2)} -/* ── Badges ── */ .badge{padding:.15rem .5rem;border-radius:12px;font-size:.66rem;font-weight:600;font-family:var(--fm)} .b-pass{background:var(--gnl);color:var(--gn)}.b-fail{background:var(--rdl);color:var(--rd)} .b-pend{background:var(--yll);color:var(--yl)}.b-rev{background:var(--bll);color:var(--bl)} .b-admin{background:var(--ppl);color:var(--pp)}.b-user{background:var(--bll);color:var(--bl)}.b-viewer{background:var(--bg3);color:var(--tm)} .b-on{background:var(--gnl);color:var(--gn)}.b-off{background:var(--bg3);color:var(--tm)} -/* ── Table ── */ table{width:100%;border-collapse:collapse;font-size:.76rem} th{text-align:left;padding:.5rem .6rem;font-size:.65rem;text-transform:uppercase;letter-spacing:.05em;color:var(--tm);border-bottom:2px solid var(--bd);font-weight:700;background:var(--bg2)} td{padding:.5rem .6rem;border-bottom:1px solid var(--bg3);color:var(--t2)} tr:hover td{background:var(--acl)} .g2{display:grid;grid-template-columns:1fr 1fr;gap:.65rem} .g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.65rem} -/* ── Chat ── */ .ch-c{display:flex;flex-direction:column;height:calc(100vh - 50px - 2.3rem)} .ch-m{flex:1;overflow-y:auto;padding:.65rem;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl) var(--rl) 0 0} -.cm{margin-bottom:.65rem;display:flex;gap:.5rem} -.cm-u{justify-content:flex-end} -.cm-u .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)} +.cm{margin-bottom:.65rem;display:flex;gap:.5rem}.cm-user{justify-content:flex-end} +.cm-user .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)} .cb{max-width:75%;padding:.55rem .75rem;border-radius:var(--rl) var(--rl) var(--rl) 4px;background:var(--bg1);font-size:.8rem;line-height:1.55;border:1px solid var(--bd);white-space:pre-wrap;box-shadow:var(--sh)} .cb code{font-family:var(--fm);font-size:.73rem;background:var(--bg3);padding:.1rem .2rem;border-radius:3px} .cb strong{color:var(--ac)} .ch-i{display:flex;gap:.35rem;padding:.55rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;border-radius:0 0 var(--rl) var(--rl)} .ch-i input{flex:1} -.ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem} +.ch-t{display:flex;gap:.35rem;align-items:center;padding:.35rem .55rem;background:var(--bg2);border:1px solid var(--bd);border-bottom:none;border-radius:var(--rl) var(--rl) 0 0;font-size:.7rem;flex-wrap:wrap} .ch-t select{max-width:240px;font-size:.72rem;padding:.25rem .4rem} .ch-t label{font-weight:600;color:var(--tm);font-size:.68rem} -/* ── Report iframe ── */ .rif{width:100%;height:calc(100vh - 50px - 5.5rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff} -/* ── Login ── */ .lp{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#fef2f0 0%,#f0f2f5 50%,#e6f1fa 100%)} .lc{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:2rem;width:100%;max-width:370px;box-shadow:0 4px 24px rgba(0,0,0,.08)} .lc h2{font-size:1.1rem;font-family:var(--fm);color:var(--ac)}.lc .st{color:var(--tm);font-size:.76rem;margin-bottom:1.15rem} -/* ── Alerts ── */ .al{padding:.55rem .75rem;border-radius:var(--r);font-size:.76rem;margin-bottom:.65rem} .al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(199,70,52,.2)} .al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(13,138,65,.2)} .al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(11,114,185,.2)} .tag{display:inline-block;padding:.1rem .35rem;background:var(--acl);color:var(--ac);border-radius:4px;font-size:.65rem;font-family:var(--fm);font-weight:500} .emp{text-align:center;padding:2rem;color:var(--tm)}.emp .eic{font-size:2rem;margin-bottom:.4rem} +.exp-r{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:.5rem;margin-top:.5rem} +.exp-c{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:.65rem;font-size:.76rem} +.exp-c strong{color:var(--t1);font-size:.8rem}.exp-c .em{font-family:var(--fm);font-size:.63rem;color:var(--tm);word-break:break-all} @keyframes fi{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.fi{animation:fi .2s ease} @keyframes pu{0%,100%{opacity:1}50%{opacity:.5}}.ld{animation:pu 1.5s infinite;color:var(--tm);font-size:.8rem;text-align:center;padding:1.2rem} ::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:var(--bg0)}::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px} @@ -97,7 +89,8 @@ tr:hover td{background:var(--acl)}