Files
A-Team-Security-Infra-Agent…/backend/app.py
nogueiraguh 2071fa037a fix: remove unavailable Cohere models and fix xAI Grok param support
- Remove 5 Cohere chat models (not available in us-ashburn-1)
- Remove 9 legacy Cohere embedding models (v3.0), keep embed-v4.0
- Update default embedding model to cohere.embed-v4.0
- Skip presence_penalty, frequency_penalty, top_k for xAI models
- Remove Cohere from chat provider order in frontend
2026-03-04 13:53:57 -03:00

1733 lines
100 KiB
Python

"""
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
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional, List, Dict, Any
from contextlib import contextmanager
from fastapi import (
FastAPI, HTTPException, Depends, Request, UploadFile, File, Form,
Query, BackgroundTasks
)
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, FileResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
import jwt as pyjwt
# ── Config ────────────────────────────────────────────────────────────────────
APP_SECRET = os.environ.get("APP_SECRET", secrets.token_hex(32))
JWT_ALG = "HS256"
JWT_EXP_H = int(os.environ.get("JWT_EXPIRY_HOURS", "12"))
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"
VERSION = "1.1"
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=VERSION)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
allow_methods=["*"], allow_headers=["*"])
security = HTTPBearer()
# ── OCI GenAI Models Catalog ──────────────────────────────────────────────────
# OCIDs are region-specific; "ocids" maps genai_region → OCID.
# _call_genai resolves the OCID for the configured region at runtime.
GENAI_MODELS = {
# ── Meta ──
"meta.llama-4-maverick-17b-128e-instruct-fp8": {"provider":"meta","name":"Meta Llama 4 Maverick","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyah6tjdejjashngznsylutuhhvufukzb2g2ls54g2flsfq"}},
"meta.llama-4-scout-17b-16e-instruct": {"provider":"meta","name":"Meta Llama 4 Scout","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaw23hfc7mtvv5wef3gwvvaguyzqmhb5lx4r5s3y2xzc4a"}},
"meta.llama-guard-4-12b": {"provider":"meta","name":"Meta Llama Guard 4 (12B)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyab5ggfxf4zs33lb5skxemyudnfxangjl4557toy3yapea"}},
# ── Google ──
"google.gemini-2.5-pro": {"provider":"google","name":"Google Gemini 2.5 Pro","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyargceyuaysrjzo2metq2rinavayxqmpu7tkm6mmfojcvq"}},
"google.gemini-2.5-flash": {"provider":"google","name":"Google Gemini 2.5 Flash","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaeo4ehrn25guuats5s45hnvswlhxo6riop275l2bkr2vq"}},
"google.gemini-2.5-flash-lite": {"provider":"google","name":"Google Gemini 2.5 Flash-Lite","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaqk3p6ljepyguyo4ff5dwjw3ecij3x4l2j32e3gz66wtq"}},
# ── OpenAI ──
"openai.gpt-5.3-codex": {"provider":"openai","name":"OpenAI GPT-5.3 Codex","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyayatqadc4zh74l6mh7sb3sb5olk7jnhq62bfnxgjwfb5a"}},
"openai.gpt-5.2-codex": {"provider":"openai","name":"OpenAI GPT-5.2 Codex","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyarqt3ngs42jvevvgunlvkb2ksxlnotqymbm4duy3phy4q"}},
"openai.gpt-5.2-pro": {"provider":"openai","name":"OpenAI GPT-5.2 Pro","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaw6eprz3l3db3w3y5udndb6fjairuepn5chunmmgwueba"}},
"openai.gpt-5.2-pro-2025-12-11": {"provider":"openai","name":"OpenAI GPT-5.2 Pro (2025-12-11)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5s4tallv2x5rh7l2zc4kaxsbkihcnhd2w6lqf3a7ty2a"}},
"openai.gpt-5.2": {"provider":"openai","name":"OpenAI GPT-5.2","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya4fw3p5fddnexbfcurnz7spgkqb4mq4a6y5ubyv7777sa"}},
"openai.gpt-5.2-2025-12-11": {"provider":"openai","name":"OpenAI GPT-5.2 (2025-12-11)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyawclpjz3ar2psi2vjqk3zwzesnq2u6uli37djpdn2zaha"}},
"openai.gpt-5.2-chat-latest": {"provider":"openai","name":"OpenAI GPT-5.2 Chat Latest","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyafvkojauwkg3b6nps2rogu5lmigedrkel65j6qtk7l2uq"}},
"openai.gpt-5.1-codex-max": {"provider":"openai","name":"OpenAI GPT-5.1 Codex Max","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaqy3hrasco26ocvumkr5canmnzkvkhgoyw6ntyvyeamrq"}},
"openai.gpt-5.1-codex": {"provider":"openai","name":"OpenAI GPT-5.1 Codex","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyar76rnxb66b4bkhlpn62jdffjedmeijbbh3h3v4e6xrxa"}},
"openai.gpt-5.1-codex-mini": {"provider":"openai","name":"OpenAI GPT-5.1 Codex Mini","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyashim6rmq4irtdxw5osv4flw6ueggq5sppyzmv3qw7tha"}},
"openai.gpt-5.1": {"provider":"openai","name":"OpenAI GPT-5.1","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya3darth2ozqcfssb2bats5jitpgigllccajasdyqljnkq"}},
"openai.gpt-5.1-2025-11-13": {"provider":"openai","name":"OpenAI GPT-5.1 (2025-11-13)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5xtecezkpnkloj5wsmgtpbz7y4n7dsryanaj3l3npk5q"}},
"openai.gpt-5.1-chat-latest": {"provider":"openai","name":"OpenAI GPT-5.1 Chat Latest","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyavtwm6mlbmpmkkvejoprbt5cpflx2bq66lgy6yr2hxsba"}},
"openai.gpt-5-codex": {"provider":"openai","name":"OpenAI GPT-5 Codex","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyasddbvtsh44glecj4bbyghxdbjabmvb76pvrkjefeqgpa"}},
"openai.gpt-5": {"provider":"openai","name":"OpenAI GPT-5","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyathaswupt2vqykuxzinzbu776zpydos343fokoddyywma"}},
"openai.gpt-5-2025-08-07": {"provider":"openai","name":"OpenAI GPT-5 (2025-08-07)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyahr5xoj3itcnd2ppl45w3isil2brd4obx2x2qslckfxsq"}},
"openai.gpt-5-mini": {"provider":"openai","name":"OpenAI GPT-5 Mini","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya3eain4n6v3edm4ryjvze5hnjouujd4vralxntfalwjaq"}},
"openai.gpt-5-mini-2025-08-07": {"provider":"openai","name":"OpenAI GPT-5 Mini (2025-08-07)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyakzv5rbuziucahmfkbrvnrzwepgfzuyio4fq4goueda2a"}},
"openai.gpt-5-nano": {"provider":"openai","name":"OpenAI GPT-5 Nano","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyactz4ikvxbjchjlkdw4jtyftsopfwk6prltzarznudqxq"}},
"openai.gpt-5-nano-2025-08-07": {"provider":"openai","name":"OpenAI GPT-5 Nano (2025-08-07)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyafyjacj4tcfrcvxpte4j5v5dhzrxs4ir7cz3hu3ztatlq"}},
"openai.gpt-4.1": {"provider":"openai","name":"OpenAI GPT-4.1","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaa6g75r2qqmtzjaooqtlxv4lxkpcqp2jdd6plpq7yq7ea"}},
"openai.gpt-4.1-2025-04-14": {"provider":"openai","name":"OpenAI GPT-4.1 (2025-04-14)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaba4ewqm32w7nsbal4od2unrtvhr6qybzfra5y2q7yhra"}},
"openai.gpt-4.1-mini": {"provider":"openai","name":"OpenAI GPT-4.1 Mini","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya6pk3sxishpiexm2rb5sf4ytb5tsbz4to2g3g23smidaa"}},
"openai.gpt-4.1-mini-2025-04-14": {"provider":"openai","name":"OpenAI GPT-4.1 Mini (2025-04-14)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyageakxq5b2haozdcjwalu6sdxsswsw3gsx4hxqaerpqvq"}},
"openai.gpt-4.1-nano": {"provider":"openai","name":"OpenAI GPT-4.1 Nano","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyacxqiaijwxalbynhst6oyg4wttzagz3dai4y2rnwh6wrq"}},
"openai.gpt-4.1-nano-2025-04-14": {"provider":"openai","name":"OpenAI GPT-4.1 Nano (2025-04-14)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaa3ubnmts4tfcwslpvuiuj74qdkyyfpb7vxo334nzzfoa"}},
"openai.gpt-4o": {"provider":"openai","name":"OpenAI GPT-4o","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyah7slrtboxdbfdy5cdspsfts62yumoclpdgwydopse7za"}},
"openai.gpt-4o-2024-08-06": {"provider":"openai","name":"OpenAI GPT-4o (2024-08-06)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyahw7ffo4apa3z5nrlfn3hjglirdasqn7ydwbe6o66tela"}},
"openai.gpt-4o-2024-11-20": {"provider":"openai","name":"OpenAI GPT-4o (2024-11-20)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyagxncxsf5vkooaj2pcrgcgjrxhexifnp2bghtnjstssga"}},
"openai.gpt-4o-mini": {"provider":"openai","name":"OpenAI GPT-4o Mini","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5jba2auifbnsmrfjqmigut4aq6gei4kxyfqmwsyjo54a"}},
"openai.gpt-4o-mini-search-preview": {"provider":"openai","name":"OpenAI GPT-4o Mini Search Preview","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyauin4xlovatfjhxp67e3pxfmjp26sxide2yobsnxn5xyq"}},
"openai.gpt-4o-mini-search-preview-2025-03-11": {"provider":"openai","name":"OpenAI GPT-4o Mini Search (2025-03-11)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyabx7ppfcp5uyt2gmfc6mrfy74lnzenayxiyfpbvo5b7ka"}},
"openai.gpt-4o-search-preview": {"provider":"openai","name":"OpenAI GPT-4o Search Preview","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyar2iwiwsqlrrdw5wmppchuz56nmkrp2soyohx3boc37mq"}},
"openai.gpt-4o-search-preview-2025-03-11": {"provider":"openai","name":"OpenAI GPT-4o Search (2025-03-11)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya7huzy6zziboyhuwfqmbhsjrz57xat5hclnjnqrnan7ha"}},
"openai.gpt-image-1.5": {"provider":"openai","name":"OpenAI GPT Image 1.5","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyauvxywwfyxnqouoltmp2oe5srzspxnnflmxpkznbwgaea"}},
"openai.gpt-image-1": {"provider":"openai","name":"OpenAI GPT Image 1","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaykmvlptihcu4iumplkemkp3u3mcw7atypidcpfpminoq"}},
"openai.gpt-audio": {"provider":"openai","name":"OpenAI GPT Audio","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyabnxjhfcolefqhio7twnnqvkxbkphpy6f5h2mo4xqkdvq"}},
"openai.o4-mini": {"provider":"openai","name":"OpenAI o4-mini","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya5ivfqp4fxlajoeg2cahlqtuuswagiv6a7dggpigy23bq"}},
"openai.o4-mini-2025-04-16": {"provider":"openai","name":"OpenAI o4-mini (2025-04-16)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyatheqm7c6zhrtgwccbwyopoi5dzrzrclw7kx5igfrqniq"}},
"openai.o3": {"provider":"openai","name":"OpenAI o3","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyalgnrukpjk6wm5zsf4jzkoneahgswhrk7kukkoagwnzma"}},
"openai.o3-2025-04-16": {"provider":"openai","name":"OpenAI o3 (2025-04-16)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyah6nkmpmrgb67u4zuxs77uc3kg3vsgmfdig5zznbtcqoq"}},
"openai.o3-mini": {"provider":"openai","name":"OpenAI o3-mini","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyayrbuyw3robmloay76lh7j2l3mk65aoy64mhcgmwzn5yq"}},
"openai.o3-mini-2025-01-31": {"provider":"openai","name":"OpenAI o3-mini (2025-01-31)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaeo3yslwf6uhxvfhbop7ukixto35432w7vir53mvw2vza"}},
"openai.o1": {"provider":"openai","name":"OpenAI o1","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya674efirmykrx77pde5ftnuihvpn45vyhn2ecj6dnl5ca"}},
"openai.o1-2024-12-17": {"provider":"openai","name":"OpenAI o1 (2024-12-17)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyarak5fe624z3kudi7p4svnlif7bgg5gpoqcxcyeoajjcq"}},
"openai.gpt-oss-120b": {"provider":"openai","name":"OpenAI GPT-oss (120B)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyachl5cngrvo4tna3bj3yqqtfvgtjarbgidmy76z7ojy6a"}},
"openai.gpt-oss-20b": {"provider":"openai","name":"OpenAI GPT-oss (20B)","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaillcx47jg3axfefkaruzmcvmse66t4nmoxuwpaqbf4kq"}},
# ── xAI ──
"xai.grok-4": {"provider":"xai","name":"xAI Grok 4","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaldmhg25is4nouena4oa2pj4nvwgfeempo4syiaazukia"}},
"xai.grok-4-1-fast-reasoning": {"provider":"xai","name":"xAI Grok 4.1 Fast Reasoning","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaocp3cooe7jkje7irb7dsrspgobjtkxakutz76gjo3k3a"}},
"xai.grok-4-1-fast-non-reasoning": {"provider":"xai","name":"xAI Grok 4.1 Fast Non-Reasoning","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaklmfkhxw5lwo5ifhqo2dxu5ymbeyzdr4nnpv3paigsoq"}},
"xai.grok-4-fast-reasoning": {"provider":"xai","name":"xAI Grok 4 Fast Reasoning","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyamqkpviarwzvo3wwuhnhqloa224fneaukac5megifuaba"}},
"xai.grok-4-fast-non-reasoning": {"provider":"xai","name":"xAI Grok 4 Fast Non-Reasoning","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaj2m67qex5dzgi7gi5blfdztnvbwggvibaxi6ac3jthha"}},
"xai.grok-3": {"provider":"xai","name":"xAI Grok 3","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyag3w2xk76vlahjujj2gdfeuzhflt25gbo3bxidlsqfjla"}},
"xai.grok-3-mini": {"provider":"xai","name":"xAI Grok 3 Mini","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyar77g5dtex3loxuluziukznryr7kowvncea33ml7vrlda"}},
"xai.grok-3-fast": {"provider":"xai","name":"xAI Grok 3 Fast","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyazdu5a5ruzfds34kwb2zwc65p6fes5uwddmkq7dyisvxq"}},
"xai.grok-3-mini-fast": {"provider":"xai","name":"xAI Grok 3 Mini Fast","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyauvjoll2repj5pbtkk7pinwj57ex3lkehzpxd6v6rxscq"}},
"xai.grok-code-fast-1": {"provider":"xai","name":"xAI Grok Code Fast 1","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyaayp5ccahfbpht56x7meyc26gyvojowmrbrr4xj626enq"}},
# ── ProtectAI ──
"protectai.deberta-v3-base-prompt-injection-v2": {"provider":"protectai","name":"ProtectAI DeBERTa Prompt Injection v2","api_format":"GENERIC",
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyasj4kxlosqvmyaxu53lrc7iddmo2owc6bizoa2y5w47oq"}},
}
EMBEDDING_MODELS = {
"cohere.embed-v4.0": {"name":"Cohere Embed v4.0 (Multimodal)","dims":1536,
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyahw4vlsxm7newcqtlgmristnwxlrxox3h7bcnlomjpgwa"}},
"openai.text-embedding-3-large": {"name":"OpenAI Text Embedding 3 Large","dims":3072,
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceya3i6o2p5h2mij6unya5bsyc46aqey5hwy3icncawo3vcq"}},
"openai.text-embedding-3-small": {"name":"OpenAI Text Embedding 3 Small","dims":1536,
"ocids":{"us-ashburn-1":"ocid1.generativeaimodel.oc1.iad.amaaaaaask7dceyarjpjyniixp4tf7kyhr5bfajxmky4sjmbki2hp55ns2pq"}},
}
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():
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
conn.execute("PRAGMA foreign_keys=ON")
try:
yield conn
conn.commit()
finally:
conn.close()
def init_db():
with db() as c:
c.executescript("""
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
username TEXT UNIQUE NOT NULL,
email TEXT,
password_hash TEXT NOT NULL, role TEXT NOT NULL DEFAULT 'viewer',
mfa_secret TEXT, mfa_enabled INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now')),
last_login TEXT
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
expires_at TEXT NOT NULL, is_active INTEGER DEFAULT 1
);
CREATE TABLE IF NOT EXISTS oci_configs (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
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,
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',
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)
);
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 adb_vector_configs (
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
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,
tools TEXT,
linked_adb_id 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,
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,
action TEXT NOT NULL, resource TEXT, details TEXT,
ip TEXT, created_at TEXT DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS config_logs (
id TEXT PRIMARY KEY,
config_type TEXT NOT NULL,
config_id TEXT NOT NULL,
config_name TEXT,
severity TEXT NOT NULL,
action TEXT NOT NULL,
message TEXT NOT NULL,
user_id TEXT,
username TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
""")
c.execute("DELETE FROM config_logs WHERE created_at < datetime('now', '-30 days')")
# ── Migrations ──
for col in ["genai_config_id TEXT", "embedding_model_id TEXT DEFAULT 'cohere.embed-v4.0'"]:
try:
c.execute(f"ALTER TABLE adb_vector_configs ADD COLUMN {col}")
except sqlite3.OperationalError:
pass
adm = c.execute("SELECT id FROM users WHERE username='admin'").fetchone()
if not adm:
c.execute(
"INSERT INTO users (id,first_name,last_name,username,email,password_hash,role) VALUES (?,?,?,?,?,?,?)",
(str(uuid.uuid4()), "Admin", "Sistema", "admin", "admin@local", _hash_pw("admin123"), "admin")
)
log.info("Default admin created: admin / admin123")
# ── 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,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()
# ── 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])
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")
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)}")
return u
return dep
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))
def _config_log(config_type, config_id, config_name, severity, action, message, uid=None, uname=None):
with db() as c:
c.execute("INSERT INTO config_logs (id,config_type,config_id,config_name,severity,action,message,user_id,username) VALUES (?,?,?,?,?,?,?,?,?)",
(str(uuid.uuid4()), config_type, config_id, config_name or "", severity, action, str(message)[:2000], uid, uname))
# ── Models ────────────────────────────────────────────────────────────────────
class LoginReq(BaseModel):
username: str; password: str; totp_code: Optional[str] = None
class RegisterReq(BaseModel):
first_name: str; last_name: str; 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 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
# Option A: saved preset
genai_config_id: Optional[str] = None
# Option B: direct model selection (inline)
model_id: Optional[str] = None; oci_config_id: Optional[str] = None
genai_region: Optional[str] = None; compartment_id: Optional[str] = None
temperature: Optional[float] = None; max_tokens: Optional[int] = None
top_p: Optional[float] = None; top_k: Optional[int] = None
frequency_penalty: Optional[float] = None; presence_penalty: Optional[float] = None
class RunReportReq(BaseModel):
config_id: str; mcp_server_id: Optional[str] = None; regions: Optional[List[str]] = None
class GenAIConfigReq(BaseModel):
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
wallet_password: Optional[str] = None; table_name: str = "CIS_EMBEDDINGS"; use_mtls: bool = True
genai_config_id: Optional[str] = None; embedding_model_id: str = "cohere.embed-v4.0"
class IngestDocReq(BaseModel):
adb_config_id: str; documents: List[Dict[str, Any]]
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; tools: Optional[List[str]] = None
linked_adb_id: 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 = 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()
with db() as c:
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"],"first_name":u["first_name"],"last_name":u["last_name"],"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"],))
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")
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,first_name,last_name,username,email,password_hash,role) VALUES (?,?,?,?,?,?,?)",
(uid, req.first_name, req.last_name, 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"]))
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"]))
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"],))
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,))
_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,first_name,last_name,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")
async def me(u=Depends(current_user)):
return {k: u[k] for k in ("id","first_name","last_name","username","email","role","mfa_enabled")}
@app.put("/api/users/{uid}")
async def update_user(uid: str, req: UserUpdateReq, adm=Depends(require("admin"))):
sets, vals = [], []
if req.email is not None: sets.append("email=?"); vals.append(req.email)
if req.role is not None:
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)
_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,))
_audit(adm["id"], adm["username"], "deactivate_user", uid)
return {"ok": True}
# ── OCI Config ────────────────────────────────────────────────────────────────
@app.post("/api/oci/config")
async def save_oci(
tenancy_name: str = Form(...), tenancy_ocid: str = Form(...),
user_ocid: str = Form(...), fingerprint: str = Form(...),
region: str = Form(...), compartment_id: str = Form(""),
key_passphrase: 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)
kp = cdir / "oci_api_key.pem"
key_bytes = await private_key.read()
kp.write_bytes(key_bytes); kp.chmod(0o600)
# Detect encrypted keys that require passphrase
key_text = key_bytes.decode("utf-8", errors="ignore")
key_is_encrypted = "ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text
if key_is_encrypted and not key_passphrase:
# Clean up and warn
shutil.rmtree(cdir, ignore_errors=True)
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
if public_key: (cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
cfg_file = cdir / "config"
cfg_content = (f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n")
if key_passphrase:
cfg_content += f"pass_phrase={key_passphrase}\n"
cfg_file.write_text(cfg_content)
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))
_audit(u["id"], u["username"], "save_oci_config", cid, f"tenancy={tenancy_name}")
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial salva: {tenancy_name} ({region})", u["id"], u["username"])
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:
cols = "id,user_id,tenancy_name,tenancy_ocid,user_ocid,fingerprint,region,compartment_id,created_at"
if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM oci_configs").fetchall()
else: rows=c.execute(f"SELECT {cols} 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()
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 d.exists(): shutil.rmtree(d)
return {"ok": True}
@app.put("/api/oci/configs/{cid}")
async def update_oci(
cid: str,
tenancy_name: str = Form(...), tenancy_ocid: str = Form(...),
user_ocid: str = Form(...), fingerprint: str = Form(...),
region: str = Form(...), compartment_id: str = Form(""),
key_passphrase: str = Form(""),
private_key: Optional[UploadFile] = File(None), public_key: Optional[UploadFile] = File(None),
u = Depends(require("admin","user"))
):
with db() as c:
existing = c.execute("SELECT * FROM oci_configs WHERE id=?", (cid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
cdir = OCI_DIR / cid; cdir.mkdir(parents=True, exist_ok=True)
kp = cdir / "oci_api_key.pem"
if private_key and private_key.filename:
key_bytes = await private_key.read()
kp.write_bytes(key_bytes); kp.chmod(0o600)
key_text = key_bytes.decode("utf-8", errors="ignore")
if ("ENCRYPTED" in key_text or "Proc-Type: 4,ENCRYPTED" in key_text) and not key_passphrase:
raise HTTPException(400, "A chave privada está criptografada (ENCRYPTED). Informe a Key Passphrase.")
if public_key and public_key.filename:
(cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
cfg_file = cdir / "config"
cfg_content = (f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\n"
f"tenancy={tenancy_ocid}\nregion={region}\nkey_file={kp}\n")
if key_passphrase:
cfg_content += f"pass_phrase={key_passphrase}\n"
cfg_file.write_text(cfg_content); cfg_file.chmod(0o600)
with db() as c:
c.execute("UPDATE oci_configs SET tenancy_name=?,tenancy_ocid=?,user_ocid=?,fingerprint=?,region=?,compartment_id=? WHERE id=?",
(tenancy_name, tenancy_ocid, user_ocid, fingerprint, region, compartment_id or None, cid))
_audit(u["id"], u["username"], "update_oci_config", cid, f"tenancy={tenancy_name}")
_config_log("oci", cid, tenancy_name, "success", "save", f"Credencial atualizada: {tenancy_name} ({region})", u["id"], u["username"])
return {"id": cid, "tenancy_name": tenancy_name, "region": region}
@app.post("/api/oci/test/{cid}")
async def test_oci(cid: str, u=Depends(require("admin","user"))):
cp = OCI_DIR / cid / "config"
cname = None
with db() as c:
row = c.execute("SELECT tenancy_name FROM oci_configs WHERE id=?", (cid,)).fetchone()
if row: cname = row["tenancy_name"]
if not cp.exists():
_config_log("oci", cid, cname, "error", "test", "Config não encontrada", u["id"], u["username"])
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, stdin=subprocess.DEVNULL)
if r.returncode == 0:
_config_log("oci", cid, cname, "success", "test", "Conexão OK", u["id"], u["username"])
return {"status":"success","message":"Conexão OK","data":json.loads(r.stdout)}
msg = r.stderr[:500]
if "passphrase" in msg.lower() or "getpass" in msg.lower():
msg = "A chave privada requer passphrase. Recadastre a credencial informando a Key Passphrase."
_config_log("oci", cid, cname, "error", "test", msg, u["id"], u["username"])
return {"status":"error","message":msg}
except FileNotFoundError:
_config_log("oci", cid, cname, "error", "test", "OCI CLI não disponível no container.", u["id"], u["username"])
return {"status":"error","message":"OCI CLI não disponível no container."}
except subprocess.TimeoutExpired:
_config_log("oci", cid, cname, "error", "test", "Timeout na conexão", u["id"], u["username"])
return {"status":"error","message":"Timeout na conexão"}
except Exception as e:
_config_log("oci", cid, cname, "error", "test", str(e)[:500], u["id"], u["username"])
return {"status":"error","message":str(e)[:500]}
# ── 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, "embedding_models": EMBEDDING_MODELS}
@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,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)
_config_log("genai", gid, req.name, "success", "save", f"Modelo salvo: {req.model_id} ({req.genai_region})", u["id"], u["username"])
return {"id": gid, "model_id": req.model_id, "endpoint": ep}
@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.put("/api/genai/configs/{gid}")
async def update_genai(gid: str, req: GenAIConfigReq, u=Depends(require("admin","user"))):
with db() as c:
existing = c.execute("SELECT * FROM genai_configs WHERE id=?", (gid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
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(
"""UPDATE genai_configs SET 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=? WHERE 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), gid))
_audit(u["id"], u["username"], "update_genai_config", gid, req.model_id)
_config_log("genai", gid, req.name, "success", "save", f"Modelo atualizado: {req.model_id} ({req.genai_region})", u["id"], u["username"])
return {"id": gid, "model_id": req.model_id, "endpoint": ep}
@app.post("/api/genai/test/{gid}")
async def test_genai(gid: str, u=Depends(require("admin","user"))):
with db() as c:
gc = c.execute("SELECT * FROM genai_configs WHERE id=?",(gid,)).fetchone()
if not gc: raise HTTPException(404)
gname = gc["name"]
try:
resp = _call_genai(dict(gc), "Say 'connection successful' in one short sentence.")
_config_log("genai", gid, gname, "success", "test", f"GenAI OK: {resp[:200]}", u["id"], u["username"])
return {"status":"success","message":"GenAI OK","response":resp[:300]}
except Exception as e:
msg = str(e)[:500]
_config_log("genai", gid, gname, "error", "test", msg, u["id"], u["username"])
return {"status":"error","message":msg}
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
# 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")
# 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 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:
# ── Generic format (Meta Llama, Google, xAI, OpenAI) ──
chat_request = oci.generative_ai_inference.models.GenericChatRequest()
chat_request.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC
provider = model_info.get("provider", "")
is_openai = provider == "openai"
is_xai = provider == "xai"
# OpenAI models use max_completion_tokens instead of max_tokens
# and do not support top_k
if is_openai:
chat_request.max_completion_tokens = int(gc.get("max_tokens", 6000))
else:
chat_request.max_tokens = int(gc.get("max_tokens", 6000))
if not is_xai:
chat_request.top_k = int(gc.get("top_k", 1))
chat_request.temperature = float(gc.get("temperature", 1))
if not is_xai:
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))
messages = []
if history:
for h in history:
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 - resolve OCID: explicit model_ocid > catalog ocid for region > short model_id
model_ref = gc.get("model_ocid") or ""
if not model_ref:
region = gc.get("genai_region", "")
ocids = model_info.get("ocids", {})
model_ref = ocids.get(region) or gc["model_id"]
log.info(f"GenAI call: model_id={gc.get('model_id')}, model_ref={model_ref[:60]}...")
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.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 resp.text if hasattr(resp, 'text') else str(resp)
else:
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)
# ── RAG Helpers ───────────────────────────────────────────────────────────────
RAG_SYSTEM_PROMPT = """You are the OCI CIS AI Agent, an expert assistant for Oracle Cloud Infrastructure security and compliance.
You have been provided with relevant context documents retrieved from a knowledge base. Use this context to provide accurate, specific answers. If the context does not contain relevant information for the question, say so and answer based on your general knowledge.
Always cite the source documents when using information from the context.
--- RETRIEVED CONTEXT ---
{context}
--- END CONTEXT ---
User question: {question}"""
def _get_adb_connection(cfg: dict):
"""Create an oracledb connection from an adb_vector_configs row."""
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"])
return oracledb.connect(**params)
def _ensure_embeddings_table(cfg: dict):
"""Create the embeddings table in ADB if it doesn't exist."""
table_name = cfg.get("table_name", "CIS_EMBEDDINGS")
emb_model = cfg.get("embedding_model_id", "cohere.embed-v4.0")
dims = EMBEDDING_MODELS.get(emb_model, {}).get("dims", 1024)
conn = _get_adb_connection(cfg)
try:
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM user_tables WHERE table_name = :1", (table_name.upper(),))
if cur.fetchone()[0] == 0:
cur.execute(f"""
CREATE TABLE {table_name} (
ID VARCHAR2(100) PRIMARY KEY,
CONTENT CLOB,
EMBEDDING VECTOR({dims}, FLOAT64),
METADATA VARCHAR2(4000),
SOURCE VARCHAR2(500),
CREATED_AT TIMESTAMP DEFAULT SYSTIMESTAMP
)
""")
conn.commit()
log.info(f"Created embeddings table: {table_name} (dims={dims})")
cur.close()
finally:
conn.close()
def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str) -> list:
"""Generate embedding using OCI GenAI embed endpoint."""
import oci
config_path = str(OCI_DIR / genai_cfg["oci_config_id"] / "config")
config = oci.config.from_file(config_path, "DEFAULT")
endpoint = genai_cfg["endpoint"]
client = oci.generative_ai_inference.GenerativeAiInferenceClient(
config=config, service_endpoint=endpoint,
retry_strategy=oci.retry.NoneRetryStrategy(), timeout=(10, 120)
)
embed_detail = oci.generative_ai_inference.models.EmbedTextDetails()
embed_detail.inputs = [text]
# Resolve OCID for embedding model by region
emb_info = EMBEDDING_MODELS.get(embedding_model_id, {})
region = genai_cfg.get("genai_region", "")
emb_ref = emb_info.get("ocids", {}).get(region) or embedding_model_id
embed_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=emb_ref)
embed_detail.compartment_id = genai_cfg["compartment_id"]
embed_detail.truncate = "NONE"
embed_detail.input_type = "SEARCH_QUERY"
response = client.embed_text(embed_detail)
return response.data.embeddings[0]
def _vector_search(cfg: dict, query_embedding: list, top_k: int = 5) -> list:
"""Search ADB vector store using cosine similarity. Returns top-K documents."""
import array
table_name = cfg.get("table_name", "CIS_EMBEDDINGS")
conn = _get_adb_connection(cfg)
try:
cur = conn.cursor()
vec = array.array('d', query_embedding)
cur.execute(f"""
SELECT ID, CONTENT, METADATA, SOURCE,
VECTOR_DISTANCE(EMBEDDING, :1, COSINE) AS distance
FROM {table_name}
ORDER BY distance ASC
FETCH FIRST :2 ROWS ONLY
""", [vec, top_k])
results = []
for row in cur:
content = row[1]
if hasattr(content, 'read'):
content = content.read()
results.append({
"id": row[0], "content": content or "",
"metadata": row[2], "source": row[3], "distance": float(row[4])
})
cur.close()
return results
finally:
conn.close()
def _build_rag_context(documents: list) -> str:
"""Format retrieved documents into a context string for the LLM prompt."""
if not documents:
return ""
parts = []
for i, doc in enumerate(documents, 1):
source = doc.get("source", "unknown")
content = doc.get("content", "")
if len(content) > 2000:
content = content[:2000] + "..."
parts.append(f"[Document {i} | Source: {source}]\n{content}")
return "\n\n---\n\n".join(parts)
def _get_active_adb_config(user_id: str) -> dict | None:
"""Get the first active ADB vector config with a linked GenAI config."""
with db() as c:
cfg = c.execute(
"SELECT * FROM adb_vector_configs WHERE user_id=? AND is_active=1 AND genai_config_id IS NOT NULL ORDER BY created_at DESC LIMIT 1",
(user_id,)
).fetchone()
if not cfg:
cfg = c.execute(
"SELECT * FROM adb_vector_configs WHERE is_active=1 AND genai_config_id IS NOT NULL ORDER BY created_at DESC LIMIT 1"
).fetchone()
return dict(cfg) if cfg else None
# ── 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,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.tools) if req.tools else None, req.linked_adb_id))
_audit(u["id"], u["username"], "register_mcp", mid, req.name)
_config_log("mcp", mid, req.name, "success", "save", f"MCP registrado: {req.name} ({req.server_type})", u["id"], u["username"])
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)
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
@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}")
async def update_mcp(mid: str, req: MCPServerReq, u=Depends(require("admin","user"))):
with db() as c:
existing = c.execute("SELECT * FROM mcp_servers WHERE id=?", (mid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
with db() as c:
c.execute(
"UPDATE mcp_servers SET name=?,description=?,server_type=?,command=?,args=?,env_vars=?,url=?,tools=?,linked_adb_id=? WHERE 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,
json.dumps(req.tools) if req.tools else None, req.linked_adb_id, mid))
_audit(u["id"], u["username"], "update_mcp", mid, req.name)
_config_log("mcp", mid, req.name, "success", "save", f"MCP atualizado: {req.name} ({req.server_type})", u["id"], u["username"])
return {"id": mid, "name": req.name, "server_type": req.server_type}
@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"))):
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)}
@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/parse-wallet")
async def parse_wallet(wallet: UploadFile = File(...), u=Depends(require("admin","user"))):
"""Extract DSN names from tnsnames.ora inside wallet ZIP (temporary parse, no save)."""
import zipfile, tempfile
try:
data = await wallet.read()
with tempfile.TemporaryDirectory() as tmp:
zp = Path(tmp) / "wallet.zip"
zp.write_bytes(data)
with zipfile.ZipFile(str(zp), 'r') as z:
z.extractall(tmp)
tns = Path(tmp) / "tnsnames.ora"
if not tns.exists():
raise HTTPException(400, "Wallet ZIP não contém tnsnames.ora")
tns_text = tns.read_text(errors="ignore")
dsn_names = re.findall(r'^(\w[\w\-.]*)\s*=', tns_text, re.MULTILINE)
if not dsn_names:
raise HTTPException(400, "Nenhum DSN encontrado no tnsnames.ora")
return {"dsn_names": dsn_names, "files": [n for n in os.listdir(tmp) if n != "wallet.zip"]}
except zipfile.BadZipFile:
raise HTTPException(400, "Arquivo não é um ZIP válido")
@app.post("/api/adb/config")
async def save_adb(
config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
password: str = Form(...), wallet_password: str = Form(""),
table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-v4.0"),
wallet: Optional[UploadFile] = File(None),
u=Depends(require("admin","user"))
):
vid = str(uuid.uuid4())
use_mtls_bool = use_mtls.lower() in ("true", "1", "yes")
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,genai_config_id,embedding_model_id) VALUES (?,?,?,?,?,?,?,?,?,?,?)",
(vid, u["id"], config_name, dsn, username, _enc(password),
_enc(wallet_password) if wallet_password else None, table_name, int(use_mtls_bool),
genai_config_id or None, embedding_model_id))
# Auto-save wallet if provided
if wallet and wallet.filename:
import zipfile
wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
zp = wdir / "wallet.zip"
zp.write_bytes(await wallet.read())
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))
_config_log("adb", vid, config_name, "success", "upload", f"Wallet enviada com a conexão", u["id"], u["username"])
_audit(u["id"], u["username"], "save_adb_config", vid, config_name)
_config_log("adb", vid, config_name, "success", "save", f"Conexão salva: {config_name} ({dsn})", u["id"], u["username"])
return {"id": vid, "config_name": config_name}
@app.post("/api/adb/{vid}/upload-wallet")
async def upload_wallet(vid: str, wallet: UploadFile = File(...), u=Depends(require("admin","user"))):
cname = None
with db() as c:
row = c.execute("SELECT config_name FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
if row: cname = row["config_name"]
try:
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))
files = os.listdir(str(wdir))
_config_log("adb", vid, cname, "success", "upload", f"Wallet enviada: {', '.join(files)}", u["id"], u["username"])
return {"ok": True, "wallet_dir": str(wdir), "files": files}
except Exception as e:
_config_log("adb", vid, cname, "error", "upload", str(e)[:500], u["id"], u["username"])
raise HTTPException(500, str(e)[:500])
@app.get("/api/adb/configs")
async def list_adb(u=Depends(current_user)):
with db() as c:
cols = "id,config_name,dsn,username,table_name,use_mtls,is_active,wallet_dir,genai_config_id,embedding_model_id,created_at"
if u["role"]=="admin": rows=c.execute(f"SELECT {cols} FROM adb_vector_configs").fetchall()
else: rows=c.execute(f"SELECT {cols} 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)
cname = cfg["config_name"]
try:
conn = _get_adb_connection(dict(cfg))
cur = conn.cursor(); cur.execute("SELECT 1 FROM DUAL"); cur.close(); conn.close()
_config_log("adb", vid, cname, "success", "test", "Conexão Autonomous DB OK!", u["id"], u["username"])
return {"status":"success","message":"Conexão Autonomous DB OK!"}
except ImportError:
_config_log("adb", vid, cname, "error", "test", "python-oracledb não disponível no container.", u["id"], u["username"])
return {"status":"error","message":"python-oracledb não disponível no container."}
except Exception as e:
msg = str(e)[:500]
_config_log("adb", vid, cname, "error", "test", msg, u["id"], u["username"])
return {"status":"error","message":msg}
@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}
@app.put("/api/adb/configs/{vid}")
async def update_adb(
vid: str,
config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
password: str = Form(""), wallet_password: str = Form(""),
table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-v4.0"),
wallet: Optional[UploadFile] = File(None),
u=Depends(require("admin","user"))
):
with db() as c:
existing = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
if not existing: raise HTTPException(404)
if u["role"] != "admin" and existing["user_id"] != u["id"]: raise HTTPException(403)
use_mtls_bool = use_mtls.lower() in ("true", "1", "yes")
sets = "config_name=?,dsn=?,username=?,table_name=?,use_mtls=?,genai_config_id=?,embedding_model_id=?"
vals = [config_name, dsn, username, table_name, int(use_mtls_bool), genai_config_id or None, embedding_model_id]
if password:
sets += ",password_enc=?"
vals.append(_enc(password))
if wallet_password:
sets += ",wallet_password_enc=?"
vals.append(_enc(wallet_password))
vals.append(vid)
with db() as c:
c.execute(f"UPDATE adb_vector_configs SET {sets} WHERE id=?", vals)
if wallet and wallet.filename:
import zipfile
wdir = WALLET_DIR / vid; wdir.mkdir(parents=True, exist_ok=True)
zp = wdir / "wallet.zip"
zp.write_bytes(await wallet.read())
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))
_config_log("adb", vid, config_name, "success", "upload", "Wallet atualizado", u["id"], u["username"])
_audit(u["id"], u["username"], "update_adb_config", vid, config_name)
_config_log("adb", vid, config_name, "success", "save", f"Conexão atualizada: {config_name} ({dsn})", u["id"], u["username"])
return {"id": vid, "config_name": config_name}
@app.post("/api/adb/{vid}/ensure-table")
async def ensure_table(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:
_ensure_embeddings_table(dict(cfg))
return {"ok": True, "table": cfg["table_name"]}
except Exception as e:
raise HTTPException(500, f"Falha ao criar tabela: {str(e)[:500]}")
@app.post("/api/adb/{vid}/ingest")
async def ingest_documents(vid: str, req: IngestDocReq, bg: BackgroundTasks, 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, "ADB config not found")
cfg = dict(cfg)
if not cfg.get("genai_config_id"):
raise HTTPException(400, "GenAI config not linked to this ADB connection")
with db() as c:
gc = c.execute("SELECT * FROM genai_configs WHERE id=?", (cfg["genai_config_id"],)).fetchone()
if not gc: raise HTTPException(400, "Linked GenAI config not found")
bg.add_task(_ingest_documents_task, cfg, dict(gc), req.documents, u["id"], u["username"])
return {"ok": True, "message": f"Ingestão iniciada para {len(req.documents)} documentos", "config_id": vid}
def _ingest_documents_task(cfg: dict, genai_cfg: dict, documents: list, user_id: str, username: str):
"""Background task: embed and insert documents into ADB via OCI GenAI."""
import array
emb_model = cfg.get("embedding_model_id", "cohere.embed-v4.0")
table_name = cfg.get("table_name", "CIS_EMBEDDINGS")
_ensure_embeddings_table(cfg)
conn = _get_adb_connection(cfg)
try:
cur = conn.cursor()
inserted = 0
for doc in documents:
try:
content = doc.get("content", "")
if not content: continue
embedding = _embed_text(content, genai_cfg, emb_model)
vec = array.array('d', embedding)
cur.execute(f"""
INSERT INTO {table_name} (ID, CONTENT, EMBEDDING, METADATA, SOURCE)
VALUES (:1, :2, :3, :4, :5)
""", [str(uuid.uuid4()), content, vec, doc.get("metadata", ""), doc.get("source", "manual_upload")])
inserted += 1
except Exception as e:
log.error(f"Failed to ingest document: {e}")
conn.commit()
cur.close()
log.info(f"Ingested {inserted}/{len(documents)} documents into {table_name}")
_audit(user_id, username, "ingest_documents", cfg["id"], f"{inserted} documents")
_config_log("adb", cfg["id"], cfg.get("config_name"), "success", "ingest", f"{inserted}/{len(documents)} documentos ingeridos em {table_name}", user_id, username)
except Exception as e:
log.error(f"Ingestion task failed: {e}")
_config_log("adb", cfg["id"], cfg.get("config_name"), "error", "ingest", str(e)[:500], user_id, username)
finally:
conn.close()
# ── Embeddings ────────────────────────────────────────────────────────────────
def _chunk_report_by_section(report_data: dict) -> list:
"""Chunk a CIS report into documents grouped by section."""
if isinstance(report_data, str):
report_data = json.loads(report_data)
findings = report_data.get("findings", {})
tenancy = report_data.get("tenancy", "unknown")
generated_at = report_data.get("generated_at", "")
sections = {}
for cid, check in findings.items():
sec = check.get("section", "Other")
sections.setdefault(sec, [])
sections[sec].append(check)
documents = []
for section_name, checks in sections.items():
passed = sum(1 for c in checks if c.get("status") == "PASS")
failed = sum(1 for c in checks if c.get("status") == "FAIL")
review = sum(1 for c in checks if c.get("status") == "REVIEW")
lines = [f"Section: {section_name}", f"Total checks: {len(checks)}, Passed: {passed}, Failed: {failed}, Review: {review}", ""]
for c in checks:
status = c.get("status", "REVIEW")
lines.append(f"- [{c.get('id', '')}] {c.get('title', '')} — Status: {status}")
if c.get("findings"):
for f in c["findings"]:
lines.append(f" Finding: {f}")
documents.append({
"content": "\n".join(lines),
"source": f"CIS Report - {tenancy} - {generated_at}",
"metadata": f"section: {section_name}, total: {len(checks)}, passed: {passed}, failed: {failed}, review: {review}"
})
return documents
def _chunk_text_file(text: str, filename: str, chunk_size: int = 1000) -> list:
"""Split text into chunks by paragraphs or fixed size."""
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
documents = []
current_chunk = ""
chunk_num = 1
for para in paragraphs:
if len(current_chunk) + len(para) + 2 > chunk_size and current_chunk:
documents.append({"content": current_chunk, "source": filename, "metadata": f"chunk: {chunk_num}"})
chunk_num += 1
current_chunk = para
else:
current_chunk = current_chunk + "\n\n" + para if current_chunk else para
if current_chunk:
documents.append({"content": current_chunk, "source": filename, "metadata": f"chunk: {chunk_num}"})
return documents
def _get_adb_and_genai(vid: str):
"""Load ADB config and its linked GenAI config. Returns (adb_cfg, genai_cfg) or raises."""
with db() as c:
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
if not cfg: raise HTTPException(404, "ADB config not found")
cfg = dict(cfg)
if not cfg.get("genai_config_id"):
raise HTTPException(400, "GenAI config not linked to this ADB connection")
with db() as c:
gc = c.execute("SELECT * FROM genai_configs WHERE id=?", (cfg["genai_config_id"],)).fetchone()
if not gc: raise HTTPException(400, "Linked GenAI config not found")
return cfg, dict(gc)
@app.post("/api/embeddings/report/{rid}")
async def embed_report(rid: str, req: dict, bg: BackgroundTasks, u=Depends(require("admin","user"))):
vid = req.get("adb_config_id")
if not vid: raise HTTPException(400, "adb_config_id is required")
with db() as c:
r = c.execute("SELECT report_data,tenancy_name FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone()
if not r: raise HTTPException(404, "Report not found or not completed")
report_data = r["report_data"]
if isinstance(report_data, str):
try: report_data = json.loads(report_data)
except: raise HTTPException(400, "Invalid report data")
documents = _chunk_report_by_section(report_data)
if not documents: raise HTTPException(400, "No sections found in report")
cfg, gc = _get_adb_and_genai(vid)
bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"])
_audit(u["id"], u["username"], "embed_report", rid, f"{len(documents)} sections")
return {"ok": True, "message": f"Embedding de {len(documents)} seções iniciado", "sections": len(documents)}
@app.post("/api/embeddings/upload")
async def embed_upload(adb_config_id: str = Form(...), file: UploadFile = File(...), bg: BackgroundTasks = None, u=Depends(require("admin","user"))):
if not file.filename.lower().endswith(".txt"):
raise HTTPException(400, "Only .txt files are supported")
content = (await file.read()).decode("utf-8", errors="replace")
if not content.strip(): raise HTTPException(400, "File is empty")
documents = _chunk_text_file(content, file.filename)
if not documents: raise HTTPException(400, "No content chunks found")
cfg, gc = _get_adb_and_genai(adb_config_id)
bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"])
_audit(u["id"], u["username"], "embed_upload", file.filename, f"{len(documents)} chunks")
return {"ok": True, "message": f"Embedding de {len(documents)} chunks iniciado", "chunks": len(documents), "filename": file.filename}
@app.get("/api/embeddings/{vid}/list")
async def list_embeddings(vid: str, limit: int = Query(50), offset: int = Query(0), u=Depends(current_user)):
with db() as c:
cfg = c.execute("SELECT * FROM adb_vector_configs WHERE id=?", (vid,)).fetchone()
if not cfg: raise HTTPException(404)
try:
conn = _get_adb_connection(dict(cfg))
cur = conn.cursor()
table_name = cfg["table_name"] or "CIS_EMBEDDINGS"
cur.execute(f"SELECT COUNT(*) FROM {table_name}")
total = cur.fetchone()[0]
cur.execute(f"""
SELECT ID, SOURCE, METADATA, CREATED_AT FROM {table_name}
ORDER BY CREATED_AT DESC
OFFSET :1 ROWS FETCH NEXT :2 ROWS ONLY
""", [offset, limit])
rows = []
for row in cur:
rows.append({"id": row[0], "source": row[1], "metadata": row[2],
"created_at": str(row[3]) if row[3] else None})
cur.close(); conn.close()
return {"total": total, "offset": offset, "limit": limit, "documents": rows}
except Exception as e:
raise HTTPException(500, f"Erro ao listar embeddings: {str(e)[:500]}")
@app.delete("/api/embeddings/{vid}/{doc_id}")
async def delete_embedding(vid: str, doc_id: 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:
conn = _get_adb_connection(dict(cfg))
cur = conn.cursor()
table_name = cfg["table_name"] or "CIS_EMBEDDINGS"
cur.execute(f"DELETE FROM {table_name} WHERE ID = :1", [doc_id])
conn.commit()
cur.close(); conn.close()
return {"ok": True}
except Exception as e:
raise HTTPException(500, f"Erro ao deletar: {str(e)[:500]}")
# ── 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()
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,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, 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)]
if mcp_server_id:
with db() as c:
mcp = c.execute("SELECT * FROM mcp_servers WHERE id=?",(mcp_server_id,)).fetchone()
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"
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))
_config_log("oci", cfg["id"], cfg["tenancy_name"], "success", "report", f"Relatório concluído: {rid}")
else:
err = (stderr.decode() if stderr else "Unknown")[:2000]
c.execute("UPDATE reports SET status='failed',error_msg=?,completed_at=datetime('now') WHERE id=?", (err, rid))
_config_log("oci", cfg["id"], cfg["tenancy_name"], "error", "report", err)
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))
_config_log("oci", cfg["id"], cfg["tenancy_name"], "error", "report", str(e)[:2000])
@app.get("/api/reports")
async def list_reports(u=Depends(current_user)):
with db() as c:
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, 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 d.get("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):
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, 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"]
if not p or not Path(p).exists(): raise HTTPException(404)
return FileResponse(p, filename=f"cis_{r['tenancy_name']}_{rid[:8]}.{fmt}")
# ── 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,model_id) VALUES (?,?,?,?,?,?)",
(str(uuid.uuid4()), sid, u["id"], "user", msg.message, None))
genai_cfg = None
if msg.genai_config_id:
with db() as c:
row = c.execute("SELECT * FROM genai_configs WHERE id=?", (msg.genai_config_id,)).fetchone()
if row:
genai_cfg = dict(row)
# Override params from inline chat settings if provided
if msg.temperature is not None: genai_cfg["temperature"] = msg.temperature
if msg.max_tokens is not None: genai_cfg["max_tokens"] = msg.max_tokens
if msg.top_p is not None: genai_cfg["top_p"] = msg.top_p
if msg.top_k is not None: genai_cfg["top_k"] = msg.top_k
if msg.frequency_penalty is not None: genai_cfg["frequency_penalty"] = msg.frequency_penalty
if msg.presence_penalty is not None: genai_cfg["presence_penalty"] = msg.presence_penalty
elif msg.model_id and msg.oci_config_id:
# Direct model mode: build synthetic config dict
with db() as c:
oci_row = c.execute("SELECT * FROM oci_configs WHERE id=?", (msg.oci_config_id,)).fetchone()
if not oci_row:
raise HTTPException(400, "OCI config not found")
region = msg.genai_region or oci_row["region"]
compartment = msg.compartment_id or oci_row.get("compartment_id") or ""
if not compartment:
raise HTTPException(400, "compartment_id required")
genai_cfg = {
"oci_config_id": msg.oci_config_id,
"model_id": msg.model_id,
"model_ocid": None,
"compartment_id": compartment,
"genai_region": region,
"endpoint": f"https://inference.generativeai.{region}.oci.oraclecloud.com",
"serving_type": "ON_DEMAND",
"dedicated_endpoint_id": None,
"temperature": msg.temperature if msg.temperature is not None else 1.0,
"max_tokens": msg.max_tokens if msg.max_tokens is not None else 6000,
"top_p": msg.top_p if msg.top_p is not None else 0.95,
"top_k": msg.top_k if msg.top_k is not None else 1,
"frequency_penalty": msg.frequency_penalty if msg.frequency_penalty is not None else 0.0,
"presence_penalty": msg.presence_penalty if msg.presence_penalty is not None else 0.0,
}
if genai_cfg:
try:
history = []
with db() as c:
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]
# ── RAG: augment with vector context if ADB config is active ──
rag_context = ""
adb_cfg = _get_active_adb_config(u["id"])
if adb_cfg:
try:
with db() as c:
emb_genai = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone()
if emb_genai:
emb_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0")
query_embedding = _embed_text(msg.message, dict(emb_genai), emb_model)
documents = _vector_search(adb_cfg, query_embedding, top_k=5)
if documents:
rag_context = _build_rag_context(documents)
log.info(f"RAG: Retrieved {len(documents)} documents for query")
except Exception as e:
log.warning(f"RAG retrieval failed (non-fatal): {e}")
augmented_message = RAG_SYSTEM_PROMPT.format(context=rag_context, question=msg.message) if rag_context else msg.message
resp = _call_genai(dict(genai_cfg), augmented_message, history[:-1] if len(history) > 1 else None)
except Exception as e:
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, mid))
return {"session_id": sid, "response": resp, "model_id": mid}
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"]
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\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"]):
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\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 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}")
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=[]
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()]
res.append(d)
return res
@app.get("/api/downloads/{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()
return [dict(r) for r in rows]
# ── Config Logs ───────────────────────────────────────────────────────────────
@app.get("/api/config-logs")
async def get_config_logs(
config_type: str = Query(None), config_id: str = Query(None),
severity: str = Query(None), limit: int = Query(50, le=200),
u=Depends(current_user)
):
query = "SELECT * FROM config_logs WHERE 1=1"
params = []
if config_type: query += " AND config_type=?"; params.append(config_type)
if config_id: query += " AND config_id=?"; params.append(config_id)
if severity: query += " AND severity=?"; params.append(severity)
if u["role"] != "admin": query += " AND user_id=?"; params.append(u["id"])
query += " ORDER BY created_at DESC LIMIT ?"
params.append(limit)
with db() as c: rows = c.execute(query, params).fetchall()
return [dict(r) for r in rows]
@app.delete("/api/config-logs")
async def clear_config_logs(
config_type: str = Query(None), config_id: str = Query(None),
u=Depends(require("admin"))
):
query = "DELETE FROM config_logs WHERE 1=1"
params = []
if config_type: query += " AND config_type=?"; params.append(config_type)
if config_id: query += " AND config_id=?"; params.append(config_id)
with db() as c: c.execute(query, params)
return {"ok": True}
# ── Health ────────────────────────────────────────────────────────────────────
@app.get("/api/health")
async def health():
return {"status":"ok","ts":datetime.utcnow().isoformat(),"version":VERSION}
@app.on_event("startup")
async def startup():
init_db()
log.info(f"OCI CIS AI Agent v{VERSION} API started")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)