656 lines
31 KiB
Python
656 lines
31 KiB
Python
"""
|
||
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.
|
||
"""
|
||
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"
|
||
|
||
for d in [DATA, OCI_DIR, REPORTS]:
|
||
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.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True,
|
||
allow_methods=["*"], allow_headers=["*"])
|
||
security = HTTPBearer()
|
||
|
||
# ── 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, 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,
|
||
created_at TEXT DEFAULT (datetime('now'))
|
||
);
|
||
CREATE TABLE IF NOT EXISTS reports (
|
||
id TEXT PRIMARY KEY, user_id TEXT NOT NULL,
|
||
tenancy_name TEXT NOT NULL, config_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 (
|
||
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,
|
||
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'))
|
||
);
|
||
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'))
|
||
);
|
||
""")
|
||
adm = c.execute("SELECT id FROM users WHERE username='admin'").fetchone()
|
||
if not adm:
|
||
c.execute(
|
||
"INSERT INTO users (id,username,email,password_hash,role) VALUES (?,?,?,?,?)",
|
||
(str(uuid.uuid4()), "admin", "admin@local", _hash_pw("admin123"), "admin")
|
||
)
|
||
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
|
||
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()
|
||
|
||
# ── 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: str, uname: str, action: str, resource: str = None,
|
||
details: str = None, ip: str = 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)
|
||
)
|
||
|
||
# ── 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
|
||
|
||
# ── 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"],"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,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"]))
|
||
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,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","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(...),
|
||
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"
|
||
kp.write_bytes(await private_key.read()); kp.chmod(0o600)
|
||
if public_key:
|
||
(cdir / "oci_api_key_public.pem").write_bytes(await public_key.read())
|
||
(cdir / "config").write_text(
|
||
f"[DEFAULT]\nuser={user_ocid}\nfingerprint={fingerprint}\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))
|
||
)
|
||
_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()
|
||
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.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"}
|
||
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)}
|
||
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)}
|
||
|
||
@app.post("/api/oci/install-cli")
|
||
async def install_cli(u=Depends(require("admin"))):
|
||
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]}
|
||
except Exception as e:
|
||
return {"status":"error","message":str(e)}
|
||
|
||
# ── 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,status) VALUES (?,?,?,?,?)",
|
||
(rid, u["id"], cfg["tenancy_name"], req.config_id, "running")
|
||
)
|
||
bg.add_task(_exec_report, rid, dict(cfg), req.regions)
|
||
_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]]):
|
||
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
|
||
)
|
||
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)
|
||
)
|
||
else:
|
||
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)
|
||
)
|
||
|
||
@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()
|
||
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()
|
||
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: 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)
|
||
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()
|
||
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}")
|
||
|
||
# ── 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}
|
||
|
||
def _agent_respond(msg: str, user: dict) -> str:
|
||
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)
|
||
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.")
|
||
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.")
|
||
|
||
@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"]))
|
||
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: str, fname: str, 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]
|
||
|
||
# ── Health ────────────────────────────────────────────────────────────────────
|
||
@app.get("/api/health")
|
||
async def health():
|
||
return {"status":"ok","ts":datetime.utcnow().isoformat()}
|
||
|
||
@app.on_event("startup")
|
||
async def startup():
|
||
init_db()
|
||
log.info("OCI CIS AI Agent API started")
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
uvicorn.run(app, host="0.0.0.0", port=8000)
|