Add files via upload
This commit is contained in:
655
app.py
Normal file
655
app.py
Normal file
@@ -0,0 +1,655 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user