diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..aad892a Binary files /dev/null and b/.DS_Store differ diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..0d17ccd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..14e54fb --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/oci-cis-agent.iml b/.idea/oci-cis-agent.iml new file mode 100644 index 0000000..8ccdc4d --- /dev/null +++ b/.idea/oci-cis-agent.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile index 95311f5..d66440c 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -7,9 +7,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl gcc libffi-dev && \ rm -rf /var/lib/apt/lists/* -# Install Python deps +# Install Python deps + OCI CLI COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +RUN pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir oci-cli # Copy app COPY app.py . diff --git a/backend/app.py b/backend/app.py index beaeb11..f24bfcf 100644 --- a/backend/app.py +++ b/backend/app.py @@ -1,10 +1,10 @@ """ -OCI CIS AI Agent - Backend API -FastAPI with JWT auth, TOTP MFA, RBAC (admin/user/viewer), OCI CLI management, -Vector DB config, CIS report execution, chat agent, downloads, audit log. +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. """ import os, json, uuid, hashlib, hmac, time, base64, struct, secrets, subprocess -import shutil, asyncio, sqlite3, logging, socket, re +import shutil, asyncio, sqlite3, logging, socket, re, importlib, sys from datetime import datetime, timedelta from pathlib import Path from typing import Optional, List, Dict, Any @@ -28,18 +28,43 @@ DATA = Path(os.environ.get("DATA_DIR", "/data")) DB_PATH = DATA / "agent.db" OCI_DIR = DATA / "oci_configs" REPORTS = DATA / "reports" +MCP_DIR = DATA / "mcp_servers" +WALLET_DIR = DATA / "wallets" -for d in [DATA, OCI_DIR, REPORTS]: +for d in [DATA, OCI_DIR, REPORTS, MCP_DIR, WALLET_DIR]: d.mkdir(parents=True, exist_ok=True) logging.basicConfig(level=logging.INFO) log = logging.getLogger("agent") -app = FastAPI(title="OCI CIS AI Agent", version="1.0.0") +app = FastAPI(title="OCI CIS AI Agent", version="2.0.0") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) security = HTTPBearer() +# ── OCI GenAI Models Catalog ────────────────────────────────────────────────── +GENAI_MODELS = { + "cohere.command-a-03-2025": {"provider":"cohere","name":"Cohere Command A","api_format":"COHERE"}, + "cohere.command-r-plus-08-2024": {"provider":"cohere","name":"Cohere Command R+ (08-2024)","api_format":"COHERE"}, + "cohere.command-r-08-2024": {"provider":"cohere","name":"Cohere Command R (08-2024)","api_format":"COHERE"}, + "meta.llama-4-maverick": {"provider":"meta","name":"Meta Llama 4 Maverick","api_format":"GENERIC"}, + "meta.llama-4-scout": {"provider":"meta","name":"Meta Llama 4 Scout","api_format":"GENERIC"}, + "meta.llama-3.3-70b-instruct": {"provider":"meta","name":"Meta Llama 3.3 (70B)","api_format":"GENERIC"}, + "meta.llama-3.2-90b-vision-instruct": {"provider":"meta","name":"Meta Llama 3.2 90B Vision","api_format":"GENERIC"}, + "meta.llama-3.1-405b-instruct": {"provider":"meta","name":"Meta Llama 3.1 (405B)","api_format":"GENERIC"}, + "google.gemini-2.5-pro": {"provider":"google","name":"Google Gemini 2.5 Pro","api_format":"GENERIC"}, + "google.gemini-2.5-flash": {"provider":"google","name":"Google Gemini 2.5 Flash","api_format":"GENERIC"}, + "xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC"}, + "xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC"}, +} + +GENAI_REGIONS = [ + "us-chicago-1","us-ashburn-1","us-phoenix-1","uk-london-1", + "eu-frankfurt-1","ap-tokyo-1","ap-osaka-1","sa-saopaulo-1", + "ca-toronto-1","ap-melbourne-1","ap-mumbai-1","eu-amsterdam-1", + "me-jeddah-1","ap-singapore-1","ap-seoul-1","sa-vinhedo-1", +] + # ── Database ────────────────────────────────────────────────────────────────── @contextmanager def db(): @@ -74,28 +99,62 @@ def init_db(): tenancy_name TEXT NOT NULL, tenancy_ocid TEXT NOT NULL, user_ocid TEXT NOT NULL, fingerprint TEXT NOT NULL, region TEXT NOT NULL, key_file_path TEXT NOT NULL, + compartment_id TEXT, created_at TEXT DEFAULT (datetime('now')) ); + CREATE TABLE IF NOT EXISTS genai_configs ( + id TEXT PRIMARY KEY, user_id TEXT NOT NULL, + oci_config_id TEXT NOT NULL, + model_id TEXT NOT NULL, + compartment_id TEXT NOT NULL, + genai_region 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, + is_default INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (oci_config_id) REFERENCES oci_configs(id) + ); CREATE TABLE IF NOT EXISTS reports ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, tenancy_name TEXT NOT NULL, config_id TEXT, + mcp_server_id TEXT, status TEXT DEFAULT 'pending', report_data TEXT, html_path TEXT, json_path TEXT, created_at TEXT DEFAULT (datetime('now')), completed_at TEXT, error_msg TEXT ); - CREATE TABLE IF NOT EXISTS vector_db_configs ( + CREATE TABLE IF NOT EXISTS adb_vector_configs ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, - db_type TEXT NOT NULL, host TEXT NOT NULL, port INTEGER NOT NULL, - database_name TEXT, collection_name TEXT, - username TEXT, password_enc TEXT, api_key_enc TEXT, - extra_config TEXT, is_active INTEGER DEFAULT 1, + config_name TEXT NOT NULL, + dsn TEXT NOT NULL, + username TEXT NOT NULL, + password_enc TEXT NOT NULL, + wallet_dir TEXT, + wallet_password_enc TEXT, + table_name TEXT DEFAULT 'CIS_EMBEDDINGS', + use_mtls INTEGER DEFAULT 1, + is_active INTEGER DEFAULT 1, + created_at TEXT DEFAULT (datetime('now')) + ); + CREATE TABLE IF NOT EXISTS mcp_servers ( + id TEXT PRIMARY KEY, user_id TEXT NOT NULL, + name TEXT NOT NULL, description TEXT, + server_type TEXT NOT NULL DEFAULT 'stdio', + command TEXT, args TEXT, env_vars TEXT, + url TEXT, module_path TEXT, + is_active INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS chat_messages ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, user_id TEXT NOT NULL, role TEXT NOT NULL, - content TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) + content TEXT NOT NULL, + model_id TEXT, + created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS audit_log ( id TEXT PRIMARY KEY, user_id TEXT, username TEXT, @@ -111,196 +170,136 @@ def init_db(): ) log.info("Default admin created: admin / admin123") -# ── Crypto ──────────────────────────────────────────────────────────────────── -def _hash_pw(pw: str) -> str: - salt = secrets.token_hex(16) - h = hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), 100_000) - return f"{salt}:{h.hex()}" - -def _verify_pw(pw: str, stored: str) -> bool: - salt, h = stored.split(":") - return hmac.compare_digest( - hashlib.pbkdf2_hmac("sha256", pw.encode(), salt.encode(), 100_000).hex(), h - ) - -def _totp_secret() -> str: - return base64.b32encode(secrets.token_bytes(20)).decode() - -def _totp_verify(secret: str, code: str, window: int = 1) -> bool: - key = base64.b32decode(secret) - now = int(time.time()) // 30 - for off in range(-window, window + 1): - msg = struct.pack(">Q", now + off) - h = hmac.new(key, msg, hashlib.sha1).digest() - o = h[-1] & 0x0F - c = str((struct.unpack(">I", h[o:o+4])[0] & 0x7FFFFFFF) % 1_000_000).zfill(6) - if hmac.compare_digest(c, code): - return True +# ── Crypto helpers ──────────────────────────────────────────────────────────── +def _hash_pw(pw): salt=secrets.token_hex(16); h=hashlib.pbkdf2_hmac("sha256",pw.encode(),salt.encode(),100_000); return f"{salt}:{h.hex()}" +def _verify_pw(pw,stored): salt,h=stored.split(":"); return hmac.compare_digest(hashlib.pbkdf2_hmac("sha256",pw.encode(),salt.encode(),100_000).hex(),h) +def _totp_secret(): return base64.b32encode(secrets.token_bytes(20)).decode() +def _totp_verify(secret,code,window=1): + key=base64.b32decode(secret); now=int(time.time())//30 + for off in range(-window,window+1): + msg=struct.pack(">Q",now+off); h=hmac.new(key,msg,hashlib.sha1).digest(); o=h[-1]&0x0F + c=str((struct.unpack(">I",h[o:o+4])[0]&0x7FFFFFFF)%1_000_000).zfill(6) + if hmac.compare_digest(c,code): return True return False - -def _totp_uri(secret: str, user: str) -> str: - return f"otpauth://totp/OCI-CIS-Agent:{user}?secret={secret}&issuer=OCI-CIS-Agent" - -def _make_token(uid: str, role: str, sid: str) -> str: - return pyjwt.encode( - {"sub": uid, "role": role, "sid": sid, - "exp": datetime.utcnow() + timedelta(hours=JWT_EXP_H), - "iat": datetime.utcnow()}, - APP_SECRET, algorithm=JWT_ALG - ) - -def _enc(v: str) -> str: - return base64.b64encode(v.encode()).decode() - -def _dec(v: str) -> str: - return base64.b64decode(v.encode()).decode() +def _totp_uri(secret,user): return f"otpauth://totp/OCI-CIS-Agent:{user}?secret={secret}&issuer=OCI-CIS-Agent" +def _make_token(uid,role,sid): + return pyjwt.encode({"sub":uid,"role":role,"sid":sid,"exp":datetime.utcnow()+timedelta(hours=JWT_EXP_H),"iat":datetime.utcnow()},APP_SECRET,algorithm=JWT_ALG) +def _enc(v): return base64.b64encode(v.encode()).decode() +def _dec(v): return base64.b64decode(v.encode()).decode() # ── Auth deps ───────────────────────────────────────────────────────────────── async def current_user(cred: HTTPAuthorizationCredentials = Depends(security)): - try: - p = pyjwt.decode(cred.credentials, APP_SECRET, algorithms=[JWT_ALG]) - except pyjwt.ExpiredSignatureError: - raise HTTPException(401, "Token expirado") - except pyjwt.InvalidTokenError: - raise HTTPException(401, "Token inválido") + try: p = pyjwt.decode(cred.credentials, APP_SECRET, algorithms=[JWT_ALG]) + except pyjwt.ExpiredSignatureError: raise HTTPException(401, "Token expirado") + except pyjwt.InvalidTokenError: raise HTTPException(401, "Token inválido") with db() as c: u = c.execute("SELECT * FROM users WHERE id=? AND is_active=1", (p["sub"],)).fetchone() s = c.execute("SELECT * FROM sessions WHERE id=? AND is_active=1", (p["sid"],)).fetchone() - if not u or not s: - raise HTTPException(401, "Sessão inválida") + if not u or not s: raise HTTPException(401, "Sessão inválida") return dict(u) def require(*roles): async def dep(u=Depends(current_user)): - if u["role"] not in roles: - raise HTTPException(403, f"Requer role: {', '.join(roles)}") + if u["role"] not in roles: raise HTTPException(403, f"Requer role: {', '.join(roles)}") return u return dep -def _audit(uid: str, uname: str, action: str, resource: str = None, - details: str = None, ip: str = None): +def _audit(uid, uname, action, resource=None, details=None, ip=None): with db() as c: - c.execute( - "INSERT INTO audit_log (id,user_id,username,action,resource,details,ip) VALUES (?,?,?,?,?,?,?)", - (str(uuid.uuid4()), uid, uname, action, resource, details, ip) - ) + c.execute("INSERT INTO audit_log (id,user_id,username,action,resource,details,ip) VALUES (?,?,?,?,?,?,?)", + (str(uuid.uuid4()), uid, uname, action, resource, details, ip)) # ── Models ──────────────────────────────────────────────────────────────────── class LoginReq(BaseModel): username: str; password: str; totp_code: Optional[str] = None - class RegisterReq(BaseModel): username: str; email: str; password: str; role: str = "viewer" - class TOTPVerify(BaseModel): totp_code: str - class ChangePwReq(BaseModel): current_password: str; new_password: str - -class VectorDBReq(BaseModel): - db_type: str; host: str; port: int - database_name: Optional[str] = None; collection_name: Optional[str] = None - username: Optional[str] = None; password: Optional[str] = None - api_key: Optional[str] = None; extra_config: Optional[Dict] = None - -class ChatMsg(BaseModel): - message: str; session_id: Optional[str] = None - -class RunReportReq(BaseModel): - config_id: str; regions: Optional[List[str]] = None - class UserUpdateReq(BaseModel): - email: Optional[str] = None; role: Optional[str] = None - is_active: Optional[bool] = None + 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 +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 + is_default: bool = False +class ADBVectorReq(BaseModel): + config_name: str; dsn: str; username: str; password: str + wallet_password: Optional[str] = None; table_name: str = "CIS_EMBEDDINGS"; use_mtls: bool = True +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 # ── Auth endpoints ──────────────────────────────────────────────────────────── @app.post("/api/auth/login") async def login(req: LoginReq, request: Request): with db() as c: - u = c.execute("SELECT * FROM users WHERE username=? AND is_active=1", - (req.username,)).fetchone() - if not u or not _verify_pw(req.password, u["password_hash"]): - raise HTTPException(401, "Credenciais inválidas") + u = c.execute("SELECT * FROM users WHERE username=? AND is_active=1", (req.username,)).fetchone() + if not u or not _verify_pw(req.password, u["password_hash"]): raise HTTPException(401, "Credenciais inválidas") u = dict(u) if u["mfa_enabled"]: - if not req.totp_code: - return {"mfa_required": True, "message": "Código MFA necessário"} - if not _totp_verify(u["mfa_secret"], req.totp_code): - raise HTTPException(401, "Código MFA inválido") - sid = str(uuid.uuid4()) - exp = (datetime.utcnow() + timedelta(hours=JWT_EXP_H)).isoformat() + if not req.totp_code: return {"mfa_required": True, "message": "Código MFA necessário"} + if not _totp_verify(u["mfa_secret"], req.totp_code): raise HTTPException(401, "Código MFA inválido") + sid = str(uuid.uuid4()); exp = (datetime.utcnow()+timedelta(hours=JWT_EXP_H)).isoformat() with db() as c: - c.execute("INSERT INTO sessions (id,user_id,expires_at) VALUES (?,?,?)", - (sid, u["id"], exp)) + c.execute("INSERT INTO sessions (id,user_id,expires_at) VALUES (?,?,?)", (sid, u["id"], exp)) c.execute("UPDATE users SET last_login=datetime('now') WHERE id=?", (u["id"],)) _audit(u["id"], u["username"], "login", ip=request.client.host if request.client else None) - return { - "token": _make_token(u["id"], u["role"], sid), - "user": {"id":u["id"],"username":u["username"],"email":u["email"], - "role":u["role"],"mfa_enabled":bool(u["mfa_enabled"])} - } + return {"token":_make_token(u["id"],u["role"],sid), + "user":{"id":u["id"],"username":u["username"],"email":u["email"],"role":u["role"],"mfa_enabled":bool(u["mfa_enabled"])}} @app.post("/api/auth/logout") async def logout_ep(u=Depends(current_user)): - with db() as c: - c.execute("UPDATE sessions SET is_active=0 WHERE user_id=?", (u["id"],)) + with db() as c: c.execute("UPDATE sessions SET is_active=0 WHERE user_id=?", (u["id"],)) return {"ok": True} @app.post("/api/auth/register") async def register(req: RegisterReq, adm=Depends(require("admin"))): - if req.role not in ("admin","user","viewer"): - raise HTTPException(400, "Role inválida") + if req.role not in ("admin","user","viewer"): raise HTTPException(400, "Role inválida") uid = str(uuid.uuid4()) with db() as c: - if c.execute("SELECT 1 FROM users WHERE username=?", (req.username,)).fetchone(): - raise HTTPException(400, "Usuário já existe") - c.execute("INSERT INTO users (id,username,email,password_hash,role) VALUES (?,?,?,?,?)", - (uid, req.username, req.email, _hash_pw(req.password), req.role)) + if c.execute("SELECT 1 FROM users WHERE username=?", (req.username,)).fetchone(): raise HTTPException(400, "Usuário já existe") + c.execute("INSERT INTO users (id,username,email,password_hash,role) VALUES (?,?,?,?,?)", (uid, req.username, req.email, _hash_pw(req.password), req.role)) _audit(adm["id"], adm["username"], "create_user", uid, f"user={req.username} role={req.role}") return {"id": uid, "username": req.username, "role": req.role} @app.post("/api/auth/change-password") async def change_pw(req: ChangePwReq, u=Depends(current_user)): - if not _verify_pw(req.current_password, u["password_hash"]): - raise HTTPException(400, "Senha atual incorreta") - with db() as c: - c.execute("UPDATE users SET password_hash=? WHERE id=?", - (_hash_pw(req.new_password), u["id"])) + if not _verify_pw(req.current_password, u["password_hash"]): raise HTTPException(400, "Senha atual incorreta") + with db() as c: c.execute("UPDATE users SET password_hash=? WHERE id=?", (_hash_pw(req.new_password), u["id"])) return {"ok": True} # ── MFA ─────────────────────────────────────────────────────────────────────── @app.post("/api/mfa/setup") async def mfa_setup(u=Depends(current_user)): sec = _totp_secret() - with db() as c: - c.execute("UPDATE users SET mfa_secret=? WHERE id=?", (sec, u["id"])) + with db() as c: c.execute("UPDATE users SET mfa_secret=? WHERE id=?", (sec, u["id"])) return {"secret": sec, "uri": _totp_uri(sec, u["username"])} @app.post("/api/mfa/verify") async def mfa_verify(req: TOTPVerify, u=Depends(current_user)): - if not u.get("mfa_secret"): - raise HTTPException(400, "Chame /api/mfa/setup primeiro") - if not _totp_verify(u["mfa_secret"], req.totp_code): - raise HTTPException(400, "Código inválido") - with db() as c: - c.execute("UPDATE users SET mfa_enabled=1 WHERE id=?", (u["id"],)) + if not u.get("mfa_secret"): raise HTTPException(400, "Chame /api/mfa/setup primeiro") + if not _totp_verify(u["mfa_secret"], req.totp_code): raise HTTPException(400, "Código inválido") + with db() as c: c.execute("UPDATE users SET mfa_enabled=1 WHERE id=?", (u["id"],)) return {"ok": True, "message": "MFA ativado"} @app.post("/api/mfa/disable/{user_id}") async def mfa_disable(user_id: str, adm=Depends(require("admin"))): - with db() as c: - c.execute("UPDATE users SET mfa_enabled=0,mfa_secret=NULL WHERE id=?", (user_id,)) + with db() as c: c.execute("UPDATE users SET mfa_enabled=0,mfa_secret=NULL WHERE id=?", (user_id,)) _audit(adm["id"], adm["username"], "disable_mfa", user_id) return {"ok": True} # ── Users ───────────────────────────────────────────────────────────────────── @app.get("/api/users") async def list_users(u=Depends(require("admin"))): - with db() as c: - rows = c.execute( - "SELECT id,username,email,role,mfa_enabled,is_active,created_at,last_login FROM users" - ).fetchall() + with db() as c: rows = c.execute("SELECT id,username,email,role,mfa_enabled,is_active,created_at,last_login FROM users").fetchall() return [dict(r) for r in rows] @app.get("/api/users/me") @@ -315,18 +314,16 @@ 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) - with db() as c: - c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals) + if sets: vals.append(uid); c_ = db() + with db() as c: + if sets: c.execute(f"UPDATE users SET {','.join(sets)} WHERE id=?", vals) _audit(adm["id"], adm["username"], "update_user", uid) return {"ok": True} @app.delete("/api/users/{uid}") async def del_user(uid: str, adm=Depends(require("admin"))): if uid == adm["id"]: raise HTTPException(400, "Não pode desativar a si mesmo") - with db() as c: - c.execute("UPDATE users SET is_active=0 WHERE id=?", (uid,)) + with db() as c: c.execute("UPDATE users SET is_active=0 WHERE id=?", (uid,)) _audit(adm["id"], adm["username"], "deactivate_user", uid) return {"ok": True} @@ -335,320 +332,438 @@ async def del_user(uid: str, adm=Depends(require("admin"))): async def save_oci( tenancy_name: str = Form(...), tenancy_ocid: str = Form(...), user_ocid: str = Form(...), fingerprint: str = Form(...), - region: str = Form(...), - private_key: UploadFile = File(...), - public_key: Optional[UploadFile] = File(None), + region: str = Form(...), compartment_id: str = Form(""), + private_key: UploadFile = File(...), public_key: Optional[UploadFile] = File(None), u = Depends(require("admin","user")) ): - cid = str(uuid.uuid4()) - cdir = OCI_DIR / cid; cdir.mkdir(parents=True) + cid = str(uuid.uuid4()); cdir = OCI_DIR / cid; cdir.mkdir(parents=True) 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()) + if public_key: (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read()) (cdir / "config").write_text( f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n" - f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n" - ) + f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n") with db() as c: - c.execute( - "INSERT INTO oci_configs (id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,key_file_path) VALUES (?,?,?,?,?,?,?,?)", - (cid, u["id"], tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, str(kp)) - ) + 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)) _audit(u["id"], u["username"], "save_oci_config", cid, f"tenancy={tenancy_name}") return {"id": cid, "tenancy_name": tenancy_name, "region": region} @app.get("/api/oci/configs") async def list_oci(u=Depends(current_user)): with db() as c: - if u["role"] == "admin": - rows = c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,created_at FROM oci_configs").fetchall() - else: - rows = c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,created_at FROM oci_configs WHERE user_id=?", (u["id"],)).fetchall() + if u["role"]=="admin": rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs").fetchall() + else: rows=c.execute("SELECT id,user_id,tenancy_name,tenancy_ocid,region,compartment_id,created_at FROM oci_configs WHERE user_id=?",(u["id"],)).fetchall() return [dict(r) for r in rows] @app.delete("/api/oci/configs/{cid}") async def del_oci(cid: str, u=Depends(require("admin","user"))): with db() as c: - cfg = c.execute("SELECT * FROM oci_configs WHERE id=?", (cid,)).fetchone() + cfg=c.execute("SELECT * FROM oci_configs WHERE id=?",(cid,)).fetchone() if not cfg: raise HTTPException(404) - if u["role"] != "admin" and cfg["user_id"] != u["id"]: - raise HTTPException(403) - c.execute("DELETE FROM oci_configs WHERE id=?", (cid,)) - d = OCI_DIR / cid + if u["role"]!="admin" and cfg["user_id"]!=u["id"]: raise HTTPException(403) + c.execute("DELETE FROM oci_configs WHERE id=?",(cid,)) + d=OCI_DIR/cid if d.exists(): shutil.rmtree(d) return {"ok": True} @app.post("/api/oci/test/{cid}") async def test_oci(cid: str, u=Depends(require("admin","user"))): cp = OCI_DIR / cid / "config" - if not cp.exists(): - return {"status":"error","message":"Config não encontrada"} + if not cp.exists(): return {"status":"error","message":"Config não encontrada"} try: - 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)} + 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"} - except subprocess.TimeoutExpired: - return {"status":"error","message":"Timeout na conexão"} - except Exception as e: - return {"status":"error","message":str(e)} + except FileNotFoundError: return {"status":"error","message":"OCI CLI não instalado. Instale via admin panel."} + except subprocess.TimeoutExpired: return {"status":"error","message":"Timeout na conexão"} + except Exception as e: return {"status":"error","message":str(e)} -@app.post("/api/oci/install-cli") -async def install_cli(u=Depends(require("admin"))): +# ── OCI GenAI Config ────────────────────────────────────────────────────────── +@app.get("/api/genai/models") +async def list_genai_models(u=Depends(current_user)): + return {"models": GENAI_MODELS, "regions": GENAI_REGIONS} + +@app.post("/api/genai/config") +async def save_genai(req: GenAIConfigReq, u=Depends(require("admin","user"))): + gid = str(uuid.uuid4()) + 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))) + _audit(u["id"], u["username"], "save_genai_config", gid, req.model_id) + return {"id": gid, "model_id": req.model_id} + +@app.get("/api/genai/configs") +async def list_genai(u=Depends(current_user)): + with db() as c: + if u["role"]=="admin": rows=c.execute("SELECT * FROM genai_configs").fetchall() + else: rows=c.execute("SELECT * FROM genai_configs WHERE user_id=?",(u["id"],)).fetchall() + return [dict(r) for r in rows] + +@app.delete("/api/genai/configs/{gid}") +async def del_genai(gid: str, u=Depends(require("admin","user"))): + with db() as c: c.execute("DELETE FROM genai_configs WHERE id=?", (gid,)) + return {"ok": True} + +@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() + if not gc: raise HTTPException(404) try: - r = subprocess.run(["pip","install","oci-cli","--break-system-packages"], - capture_output=True, text=True, timeout=300) - ok = r.returncode == 0 - _audit(u["id"], u["username"], "install_oci_cli", details="success" if ok else r.stderr[:200]) - return {"status":"success" if ok else "error", - "message":"OCI CLI instalado!" if ok else r.stderr[:500]} + resp = await _call_genai(dict(gc), "Say 'connection successful' in one sentence.") + return {"status":"success","message":"GenAI OK","response":resp[:200]} except Exception as e: - return {"status":"error","message":str(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" + + 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"] + } + + endpoint = f"https://inference.generativeai.{gc['genai_region']}.oci.oraclecloud.com" + client = oci.generative_ai_inference.GenerativeAiInferenceClient(config, service_endpoint=endpoint) + + 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" + ) + else: + msgs = [] + 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" + ) + + 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 + + if api_format == "COHERE": + return chat_response.text if hasattr(chat_response, 'text') else str(chat_response) + 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) + +# ── MCP Servers ─────────────────────────────────────────────────────────────── +@app.post("/api/mcp/servers") +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 (?,?,?,?,?,?,?,?,?,?)", + (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)) + _audit(u["id"], u["username"], "register_mcp", mid, req.name) + return {"id": mid, "name": req.name, "server_type": req.server_type} + +@app.get("/api/mcp/servers") +async def list_mcp(u=Depends(current_user)): + with db() as c: + if u["role"]=="admin": rows=c.execute("SELECT * FROM mcp_servers ORDER BY created_at DESC").fetchall() + else: rows=c.execute("SELECT * FROM mcp_servers WHERE user_id=? ORDER BY created_at DESC",(u["id"],)).fetchall() + 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"]) + res.append(d) + return res + +@app.delete("/api/mcp/servers/{mid}") +async def del_mcp(mid: str, u=Depends(require("admin","user"))): + with db() as c: c.execute("DELETE FROM mcp_servers WHERE id=?", (mid,)) + return {"ok": True} + +@app.put("/api/mcp/servers/{mid}/toggle") +async def toggle_mcp(mid: str, u=Depends(require("admin","user"))): + with db() as c: + cur = c.execute("SELECT is_active FROM mcp_servers WHERE id=?",(mid,)).fetchone() + if not cur: raise HTTPException(404) + c.execute("UPDATE mcp_servers SET is_active=? WHERE id=?",(0 if cur["is_active"] else 1, mid)) + return {"ok": True, "is_active": not cur["is_active"]} + +@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)) + return {"ok": True, "path": str(fp)} + +# ── ADB Vector DB Config ───────────────────────────────────────────────────── +@app.post("/api/adb/config") +async def save_adb(req: ADBVectorReq, u=Depends(require("admin","user"))): + vid = str(uuid.uuid4()) + with db() as c: + c.execute( + "INSERT INTO adb_vector_configs (id,user_id,config_name,dsn,username,password_enc,wallet_password_enc,table_name,use_mtls) VALUES (?,?,?,?,?,?,?,?,?)", + (vid, u["id"], req.config_name, req.dsn, req.username, _enc(req.password), + _enc(req.wallet_password) if req.wallet_password else None, req.table_name, int(req.use_mtls))) + _audit(u["id"], u["username"], "save_adb_config", vid, req.config_name) + return {"id": vid, "config_name": req.config_name} + +@app.post("/api/adb/{vid}/upload-wallet") +async def upload_wallet(vid: str, wallet: UploadFile = File(...), u=Depends(require("admin","user"))): + wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True) + zp = wdir / "wallet.zip" + zp.write_bytes(await wallet.read()) + import zipfile + with zipfile.ZipFile(str(zp), 'r') as z: z.extractall(str(wdir)) + with db() as c: c.execute("UPDATE adb_vector_configs SET wallet_dir=? WHERE id=?", (str(wdir), vid)) + return {"ok": True, "wallet_dir": str(wdir), "files": os.listdir(str(wdir))} + +@app.get("/api/adb/configs") +async def list_adb(u=Depends(current_user)): + with db() as c: + if u["role"]=="admin": rows=c.execute("SELECT id,config_name,dsn,username,table_name,use_mtls,is_active,wallet_dir,created_at FROM adb_vector_configs").fetchall() + else: rows=c.execute("SELECT id,config_name,dsn,username,table_name,use_mtls,is_active,wallet_dir,created_at FROM adb_vector_configs WHERE user_id=?",(u["id"],)).fetchall() + return [dict(r) for r in rows] + +@app.post("/api/adb/test/{vid}") +async def test_adb(vid: str, u=Depends(require("admin","user"))): + with db() as c: + cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?",(vid,)).fetchone() + if not cfg: raise HTTPException(404) + try: + import oracledb + params = {"user": cfg["username"], "password": _dec(cfg["password_enc"]), "dsn": cfg["dsn"]} + if cfg["use_mtls"] and cfg.get("wallet_dir"): + params["config_dir"] = cfg["wallet_dir"] + params["wallet_location"] = cfg["wallet_dir"] + if cfg.get("wallet_password_enc"): + params["wallet_password"] = _dec(cfg["wallet_password_enc"]) + conn = oracledb.connect(**params) + 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"} + except Exception as e: + return {"status":"error","message":str(e)[:500]} + +@app.delete("/api/adb/configs/{vid}") +async def del_adb(vid: str, u=Depends(require("admin","user"))): + with db() as c: c.execute("DELETE FROM adb_vector_configs WHERE id=?", (vid,)) + d = WALLET_DIR / vid + if d.exists(): shutil.rmtree(d) + return {"ok": True} # ── Reports ─────────────────────────────────────────────────────────────────── @app.post("/api/reports/run") async def run_report(req: RunReportReq, bg: BackgroundTasks, u=Depends(require("admin","user"))): with db() as c: - cfg = c.execute("SELECT * FROM oci_configs WHERE id=?", (req.config_id,)).fetchone() + cfg = c.execute("SELECT * FROM oci_configs WHERE id=?",(req.config_id,)).fetchone() if not cfg: raise HTTPException(404, "Config não encontrada") rid = str(uuid.uuid4()) - c.execute( - "INSERT INTO reports (id,user_id,tenancy_name,config_id,status) VALUES (?,?,?,?,?)", - (rid, u["id"], cfg["tenancy_name"], req.config_id, "running") - ) - bg.add_task(_exec_report, rid, dict(cfg), req.regions) + c.execute("INSERT INTO reports (id,user_id,tenancy_name,config_id,mcp_server_id,status) VALUES (?,?,?,?,?,?)", + (rid, u["id"], cfg["tenancy_name"], req.config_id, req.mcp_server_id, "running")) + bg.add_task(_exec_report, rid, dict(cfg), req.regions, req.mcp_server_id) _audit(u["id"], u["username"], "run_report", rid) return {"report_id": rid, "status": "running"} -async def _exec_report(rid: str, cfg: dict, regions: Optional[List[str]]): +async def _exec_report(rid, cfg, regions, mcp_server_id): rdir = REPORTS / rid; rdir.mkdir(parents=True, exist_ok=True) config_path = str(OCI_DIR / cfg["id"] / "config") try: cmd = ["python3", "/app/cis_runner.py", "--config", config_path, "--output", str(rdir), "--tenancy-name", cfg["tenancy_name"]] - if regions: - cmd += ["--regions", ",".join(regions)] - proc = await asyncio.create_subprocess_exec( - *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE - ) + if regions: cmd += ["--regions", ",".join(regions)] + 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"]] + 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" with db() as c: if proc.returncode == 0 and jp.exists(): - c.execute( - "UPDATE reports SET status='completed',report_data=?,html_path=?,json_path=?,completed_at=datetime('now') WHERE id=?", - (jp.read_text()[:500_000], str(hp) if hp.exists() else None, str(jp), rid) - ) + c.execute("UPDATE reports SET status='completed',report_data=?,html_path=?,json_path=?,completed_at=datetime('now') WHERE id=?", + (jp.read_text()[:500_000], str(hp) if hp.exists() else None, str(jp), rid)) else: - c.execute( - "UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", - ((stderr.decode() if stderr else "Unknown")[:2000], rid) - ) + c.execute("UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", + ((stderr.decode() if stderr else "Unknown")[:2000], rid)) except Exception as e: with db() as c: - c.execute( - "UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", - (str(e)[:2000], rid) - ) + c.execute("UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", (str(e)[:2000], rid)) @app.get("/api/reports") async def list_reports(u=Depends(current_user)): with db() as c: - q = "SELECT id,user_id,tenancy_name,status,created_at,completed_at,error_msg FROM reports" - rows = c.execute(q + " ORDER BY created_at DESC").fetchall() if u["role"]=="admin" \ - else c.execute(q + " WHERE user_id=? ORDER BY created_at DESC", (u["id"],)).fetchall() + q = "SELECT id,user_id,tenancy_name,status,mcp_server_id,created_at,completed_at,error_msg FROM reports" + rows = c.execute(q+" ORDER BY created_at DESC").fetchall() if u["role"]=="admin" \ + else c.execute(q+" WHERE user_id=? ORDER BY created_at DESC",(u["id"],)).fetchall() return [dict(r) for r in rows] @app.get("/api/reports/{rid}") -async def get_report(rid: str, u=Depends(current_user)): - with db() as c: - r = c.execute("SELECT * FROM reports WHERE id=?", (rid,)).fetchone() +async def get_report(rid, u=Depends(current_user)): + with db() as c: r=c.execute("SELECT * FROM reports WHERE id=?",(rid,)).fetchone() if not r: raise HTTPException(404) - if u["role"] != "admin" and r["user_id"] != u["id"]: raise HTTPException(403) - d = dict(r) + if u["role"]!="admin" and r["user_id"]!=u["id"]: raise HTTPException(403) + d=dict(r) if d.get("report_data"): - try: d["report_data"] = json.loads(d["report_data"]) + try: d["report_data"]=json.loads(d["report_data"]) except: pass return d @app.get("/api/reports/{rid}/html") -async def report_html(rid: str): - with db() as c: - r = c.execute("SELECT html_path FROM reports WHERE id=?", (rid,)).fetchone() - if not r or not r["html_path"] or not Path(r["html_path"]).exists(): - raise HTTPException(404) +async def report_html(rid): + with db() as c: r=c.execute("SELECT html_path FROM reports WHERE id=?",(rid,)).fetchone() + if not r or not r["html_path"] or not Path(r["html_path"]).exists(): raise HTTPException(404) return FileResponse(r["html_path"], media_type="text/html") @app.get("/api/reports/{rid}/download") -async def report_dl(rid: str, fmt: str = Query("json"), u=Depends(current_user)): - with db() as c: - r = c.execute("SELECT * FROM reports WHERE id=?", (rid,)).fetchone() +async def report_dl(rid, fmt: str = Query("json"), u=Depends(current_user)): + with db() as c: r=c.execute("SELECT * FROM reports WHERE id=?",(rid,)).fetchone() if not r: raise HTTPException(404) - p = r["json_path"] if fmt == "json" else r["html_path"] + p = r["json_path"] if fmt=="json" else r["html_path"] if not p or not Path(p).exists(): raise HTTPException(404) return FileResponse(p, filename=f"cis_{r['tenancy_name']}_{rid[:8]}.{fmt}") -# ── Vector DB ───────────────────────────────────────────────────────────────── -@app.post("/api/vectordb/config") -async def save_vdb(req: VectorDBReq, u=Depends(require("admin","user"))): - vid = str(uuid.uuid4()) - with db() as c: - c.execute( - "INSERT INTO vector_db_configs (id,user_id,db_type,host,port,database_name,collection_name,username,password_enc,api_key_enc,extra_config) VALUES (?,?,?,?,?,?,?,?,?,?,?)", - (vid, u["id"], req.db_type, req.host, req.port, req.database_name, - req.collection_name, req.username, - _enc(req.password) if req.password else None, - _enc(req.api_key) if req.api_key else None, - json.dumps(req.extra_config) if req.extra_config else None) - ) - _audit(u["id"], u["username"], "save_vectordb", vid, req.db_type) - return {"id": vid, "db_type": req.db_type, "host": req.host} - -@app.get("/api/vectordb/configs") -async def list_vdb(u=Depends(current_user)): - with db() as c: - rows = c.execute( - "SELECT id,db_type,host,port,database_name,collection_name,is_active,created_at FROM vector_db_configs WHERE user_id=? OR ?='admin'", - (u["id"], u["role"]) - ).fetchall() - return [dict(r) for r in rows] - -@app.post("/api/vectordb/test/{vid}") -async def test_vdb(vid: str, u=Depends(require("admin","user"))): - with db() as c: - cfg = c.execute("SELECT * FROM vector_db_configs WHERE id=?", (vid,)).fetchone() - if not cfg: raise HTTPException(404) - try: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.settimeout(5) - ok = s.connect_ex((cfg["host"], cfg["port"])) == 0; s.close() - return {"status":"success" if ok else "error", - "message":f"{'Conectado' if ok else 'Falha'} {cfg['host']}:{cfg['port']}"} - except Exception as e: - return {"status":"error","message":str(e)} - -@app.delete("/api/vectordb/configs/{vid}") -async def del_vdb(vid: str, u=Depends(require("admin","user"))): - with db() as c: - c.execute("DELETE FROM vector_db_configs WHERE id=?", (vid,)) - return {"ok": True} - # ── Chat Agent ──────────────────────────────────────────────────────────────── @app.post("/api/chat") 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) VALUES (?,?,?,?,?)", - (str(uuid.uuid4()), sid, u["id"], "user", msg.message)) - resp = _agent_respond(msg.message, u) - with db() as c: - c.execute("INSERT INTO chat_messages (id,session_id,user_id,role,content) VALUES (?,?,?,?,?)", - (str(uuid.uuid4()), sid, u["id"], "assistant", resp)) - return {"session_id": sid, "response": resp} + 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)) -def _agent_respond(msg: str, user: dict) -> str: + 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() + + 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() + 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) + except Exception as e: + resp = f"❌ Erro GenAI: {str(e)[:300]}" + else: + resp = _agent_respond(msg.message, u) + + 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} + +def _agent_respond(msg, user): m = msg.lower().strip() if any(k in m for k in ["status","health","como está","saúde"]): cli = "✅ Instalado" if shutil.which("oci") else "❌ Não instalado" with db() as c: - nc = c.execute("SELECT COUNT(*) n FROM oci_configs").fetchone()["n"] - nr = c.execute("SELECT COUNT(*) n FROM reports").fetchone()["n"] - nu = c.execute("SELECT COUNT(*) n FROM users WHERE is_active=1").fetchone()["n"] - return (f"📊 **Status do Sistema**\n\n" - f"• OCI CLI: {cli}\n• Configs OCI: {nc}\n• Relatórios: {nr}\n" - f"• Usuários ativos: {nu}\n• Servidor: ✅ Online") - if any(k in m for k in ["listar config","list config","mostrar config"]): - with db() as c: - rows = c.execute("SELECT tenancy_name,region,created_at FROM oci_configs").fetchall() - if not rows: return "Nenhuma configuração OCI. Vá até **Config OCI** para adicionar." - lines = ["📋 **Configurações OCI:**\n"] - for r in rows: - lines.append(f"• **{r['tenancy_name']}** — `{r['region']}` ({r['created_at']})") - return "\n".join(lines) + nc=c.execute("SELECT COUNT(*) n FROM oci_configs").fetchone()["n"] + nr=c.execute("SELECT COUNT(*) n FROM reports").fetchone()["n"] + 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") 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` — Visão geral dos checks CIS 3.0\n" - "• `gerar report` — Instruções para executar relatório\n" - "• `ajuda` — Esta mensagem\n\n" - "Use as abas laterais para navegar entre as funcionalidades.") + 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"]): + lines = ["🧠 **Modelos OCI Generative AI disponíveis:**\n"] + for mid, info in GENAI_MODELS.items(): + lines.append(f"• `{mid}` — {info['name']} ({info['provider']})") + lines.append("\nConfigure em **GenAI Config** para usar no chat.") + return "\n".join(lines) if any(k in m for k in ["cis","benchmark","checks","verificar"]): - return ("🔒 **CIS OCI Foundations Benchmark 3.0**\n\n" - "54 controles em 8 domínios:\n" - "• IAM (1.1–1.17): 17 controles\n" - "• Networking (2.1–2.8): 8 controles\n" - "• Compute (3.1–3.3): 3 controles\n" - "• Logging & Monitoring (4.1–4.18): 18 controles\n" - "• Storage Object (5.1.1–5.1.3): 3 controles\n" - "• Storage Block (5.2.1–5.2.2): 2 controles\n" - "• Storage FSS (5.3.1): 1 controle\n" - "• Asset Management (6.1–6.2): 2 controles\n\n" - "Configure uma conexão OCI e execute um relatório na aba **Report**.") - if any(k in m for k in ["report","relatório","executar","rodar","gerar"]): - return ("Para gerar um relatório CIS:\n\n" - "1. Vá em **Config OCI** e adicione suas credenciais\n" - "2. Vá em **Report** e selecione a configuração\n" - "3. Clique em **Executar CIS Compliance**\n" - "4. Acompanhe o status em **Downloads**\n\n" - "O relatório HTML será gerado automaticamente.") - return ("Entendi sua mensagem. Sou o **AI Agent de Infraestrutura & Segurança OCI**.\n\n" - "Posso ajudar com:\n" - "🔍 Compliance CIS Benchmark 3.0\n" - "📊 Relatórios de segurança\n" - "🔧 Configuração OCI e Vector DB\n" - "📋 Análise de findings\n\n" - "Digite **ajuda** para ver os comandos.") + 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" + "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}") -async def clear_chat(sid: str, u=Depends(current_user)): - with db() as c: - c.execute("DELETE FROM chat_messages WHERE session_id=? AND user_id=?", (sid, u["id"])) +async def clear_chat(sid, u=Depends(current_user)): + with db() as c: c.execute("DELETE FROM chat_messages WHERE session_id=? AND user_id=?", (sid, u["id"])) return {"ok": True} # ── Downloads ───────────────────────────────────────────────────────────────── @app.get("/api/downloads") async def list_dl(u=Depends(current_user)): with db() as c: - q = "SELECT id,tenancy_name,status,created_at,completed_at FROM reports WHERE status='completed'" - rows = c.execute(q + " ORDER BY created_at DESC").fetchall() if u["role"]=="admin" \ - else c.execute(q + " AND user_id=? ORDER BY created_at DESC", (u["id"],)).fetchall() - res = [] + q="SELECT id,tenancy_name,status,created_at,completed_at FROM reports WHERE status='completed'" + rows=c.execute(q+" ORDER BY created_at DESC").fetchall() if u["role"]=="admin" \ + else c.execute(q+" AND user_id=? ORDER BY created_at DESC",(u["id"],)).fetchall() + res=[] for r in rows: - d = dict(r); d["files"] = [] - rd = REPORTS / d["id"] - if rd.exists(): - d["files"] = [{"name":f.name,"size":f.stat().st_size} for f in rd.iterdir() if f.is_file()] + d=dict(r); d["files"]=[] + rd=REPORTS/d["id"] + if rd.exists(): d["files"]=[{"name":f.name,"size":f.stat().st_size} for f in rd.iterdir() if f.is_file()] res.append(d) return res @app.get("/api/downloads/{rid}/{fname}") -async def dl_file(rid: str, fname: str, u=Depends(current_user)): - p = REPORTS / rid / fname +async def dl_file(rid, fname, u=Depends(current_user)): + p=REPORTS/rid/fname if not p.exists(): raise HTTPException(404) return FileResponse(p, filename=fname) # ── Audit ───────────────────────────────────────────────────────────────────── @app.get("/api/audit-log") -async def audit_log(limit: int = Query(100, le=500), u=Depends(require("admin"))): - with db() as c: - rows = c.execute("SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall() +async def audit_log(limit:int=Query(100,le=500), u=Depends(require("admin"))): + with db() as c: rows=c.execute("SELECT * FROM audit_log ORDER BY created_at DESC LIMIT ?",(limit,)).fetchall() return [dict(r) for r in rows] # ── Health ──────────────────────────────────────────────────────────────────── @app.get("/api/health") async def health(): - return {"status":"ok","ts":datetime.utcnow().isoformat()} + return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":"2.0.0"} @app.on_event("startup") async def startup(): init_db() - log.info("OCI CIS AI Agent API started") + log.info("OCI CIS AI Agent v2 API started") if __name__ == "__main__": import uvicorn diff --git a/backend/requirements.txt b/backend/requirements.txt index 6890d32..39b29ed 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,3 +3,5 @@ uvicorn[standard]==0.30.0 python-multipart==0.0.12 PyJWT==2.9.0 python-dotenv==1.0.1 +oci==2.133.0 +oracledb==2.4.1 diff --git a/docker-compose.yml b/docker-compose.yml index 0e9c041..d71399f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: backend: build: diff --git a/frontend/index.html b/frontend/index.html index 0c04ed6..9913888 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,354 +6,378 @@ OCI CIS AI Agent