Add files via upload

This commit is contained in:
nogueiragustavo
2026-02-26 19:47:52 -03:00
committed by GitHub
parent 3a8e8a5d54
commit aa561a87f3
8 changed files with 1453 additions and 3 deletions

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM python:3.12-slim
WORKDIR /app
# Install system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
curl gcc libffi-dev && \
rm -rf /var/lib/apt/lists/*
# Install Python deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy app
COPY app.py .
COPY cis_runner.py .
# Data volume
RUN mkdir -p /data
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

220
README.md
View File

@@ -1,2 +1,218 @@
# oci-cis-agent # 🛡️ OCI CIS AI Agent
OCI CIS AI Agent - Infrastructure & Security Engineer. CIS Benchmark 3.0 compliance with Docker, MFA, RBAC.
**AI Agent - Infrastructure & Security Engineer**
Plataforma web Dockerizada para auditoria de segurança OCI baseada no **CIS Foundations Benchmark 3.0**, com interface de chat AI Agent, geração de relatórios HTML/JSON, gerenciamento de configurações OCI, integração com bancos vetoriais e controle de acesso RBAC com MFA.
---
## 📋 Funcionalidades
### Interface Web (4 abas principais + 3 admin)
| Aba | Descrição |
|-----|-----------|
| **💬 Chat Agent** | Interface de chat com AI Agent para consultas sobre CIS, status do sistema e comandos |
| **📊 Report** | Visualização de relatórios HTML de compliance com iframe integrado |
| **📁 Downloads** | Lista de relatórios com filtro, download em JSON/HTML |
| **☁️ Config OCI** | Gerenciamento de credenciais OCI (tenancy, OCID, chaves PEM, região) |
| **🔗 Vector DB** | Configuração de conexão com bancos vetoriais (ChromaDB, Pinecone, Weaviate, PGVector, Qdrant, Milvus, Oracle 23ai) |
| **👥 Usuários** | *(admin)* Gerenciamento de usuários, criação, roles, ativação/desativação |
| **🔐 MFA** | Configuração de autenticação TOTP (Google Authenticator / Authy) |
| **📋 Audit Log** | *(admin)* Log de auditoria de todas as ações do sistema |
### Segurança
- **Autenticação JWT** com expiração configurável
- **MFA/TOTP** compatível com Google Authenticator e Authy
- **3 roles**: `admin`, `user`, `viewer`
- **Audit log** completo de todas as ações
- **Senhas com PBKDF2-SHA256** (100k iterações + salt)
### Roles e Permissões
| Role | Permissões |
|------|-----------|
| `admin` | Acesso total: usuários, configs, relatórios, MFA, audit, instalar OCI CLI |
| `user` | Criar configs OCI/VectorDB, executar relatórios, usar chat |
| `viewer` | Visualizar relatórios, downloads, usar chat (somente leitura) |
---
## 🚀 Quick Start
### Pré-requisitos
- Docker e Docker Compose instalados
### 1. Clone e configure
```bash
git clone <seu-repo>
cd oci-agent
# Crie o arquivo de ambiente
cp .env.example .env
# Edite o .env e defina um APP_SECRET forte
```
### 2. Suba os containers
```bash
docker compose up -d --build
```
### 3. Acesse
Abra o navegador em **http://localhost:8080**
**Credenciais padrão:**
- Usuário: `admin`
- Senha: `admin123`
> ⚠️ **Altere a senha padrão imediatamente após o primeiro login!**
---
## 📐 Arquitetura
```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Browser │────▶│ Nginx │────▶│ FastAPI │
│ (Frontend) │ :80 │ (Frontend) │ │ (Backend) │
│ SPA HTML │ │ Reverse │ │ Auth/API │
└──────────────┘ │ Proxy /api │ │ SQLite DB │
└──────────────┘ └──────┬───────┘
┌───────▼───────┐
│ /data vol │
│ ├─ agent.db │
│ ├─ oci_configs│
│ ├─ reports │
└───────────────┘
```
### Containers
| Container | Imagem | Porta | Função |
|-----------|--------|-------|--------|
| `oci-agent-frontend` | `nginx:alpine` | 8080→80 | Serve o SPA + reverse proxy para API |
| `oci-agent-backend` | `python:3.12-slim` | 8000 | FastAPI com auth, RBAC, lógica de negócio |
### Volume persistente
- `agent-data``/data` no backend (SQLite, configs OCI, relatórios)
---
## 🔌 Integração com MCP Server
O sistema foi projetado para ser conectado ao **MCP Server FastMCP** criado anteriormente. Para integrar:
### 1. Monte o MCP server no container
No `docker-compose.yml`, adicione ao serviço `backend`:
```yaml
volumes:
- agent-data:/data
- ./mcp_server/server.py:/app/mcp_server.py:ro # MCP server
```
### 2. Substitua o runner stub
Edite `backend/cis_runner.py` para importar e usar as tools do MCP server:
```python
from mcp_server import connect, collect_all_data, run_cis_checks, export_report_json
```
### 3. Ou conecte via SSE/stdio
O MCP server pode ser exposto como serviço separado e o backend se conecta via protocolo MCP padrão.
---
## 🗄️ Integração Vector DB
A aba **Vector DB** permite configurar conexão com:
| Banco | Porta Padrão | Uso |
|-------|-------------|-----|
| ChromaDB | 8000 | Embeddings locais |
| Pinecone | 443 | SaaS vetorial |
| Weaviate | 8080 | Vetorial + busca híbrida |
| PGVector | 5432 | PostgreSQL com extensão vetorial |
| Qdrant | 6333 | Vetorial open-source |
| Milvus | 19530 | Vetorial distribuído |
| Oracle 23ai | 1521 | Oracle Database com AI Vector Search |
Use para armazenar e consultar findings de compliance, embeddings de documentação CIS, e contexto para o AI Agent.
---
## 📊 Relatórios CIS
O sistema gera relatórios compatíveis com o **CIS OCI Foundations Benchmark 3.0** contendo:
- **54 controles** em 8 domínios
- Relatório HTML estilizado (similar ao Oracle Cloud Security Assessment)
- Relatório JSON para integração programática
- Resumo por domínio com contagem de PASS/FAIL
- Detalhamento de cada controle
---
## 🔧 Configuração Avançada
### Variáveis de Ambiente
| Variável | Padrão | Descrição |
|----------|--------|-----------|
| `APP_SECRET` | (gerado) | Chave secreta para JWT |
| `JWT_EXPIRY_HOURS` | 12 | Expiração do token JWT |
| `PORT` | 8080 | Porta do frontend |
| `DATA_DIR` | /data | Diretório de dados persistentes |
### Instalar OCI CLI no container
Na interface como admin, vá em **Config OCI****Instalar OCI CLI no Container**.
Ou manualmente:
```bash
docker exec -it oci-agent-backend pip install oci-cli
```
### Backup dos dados
```bash
docker cp oci-agent-backend:/data ./backup-data
```
---
## 📝 API Endpoints
| Método | Endpoint | Auth | Descrição |
|--------|----------|------|-----------|
| POST | `/api/auth/login` | - | Login (retorna JWT) |
| POST | `/api/auth/logout` | ✅ | Logout |
| POST | `/api/auth/register` | admin | Criar usuário |
| POST | `/api/auth/change-password` | ✅ | Alterar senha |
| POST | `/api/mfa/setup` | ✅ | Gerar secret MFA |
| POST | `/api/mfa/verify` | ✅ | Ativar MFA |
| GET | `/api/users` | admin | Listar usuários |
| GET | `/api/users/me` | ✅ | Dados do usuário atual |
| PUT | `/api/users/{id}` | admin | Atualizar usuário |
| POST | `/api/oci/config` | user+ | Salvar config OCI (multipart) |
| GET | `/api/oci/configs` | ✅ | Listar configs |
| POST | `/api/oci/test/{id}` | user+ | Testar conexão OCI |
| POST | `/api/reports/run` | user+ | Executar relatório CIS |
| GET | `/api/reports` | ✅ | Listar relatórios |
| GET | `/api/reports/{id}/html` | ✅ | Relatório HTML |
| GET | `/api/reports/{id}/download` | ✅ | Download relatório |
| POST | `/api/vectordb/config` | user+ | Salvar config Vector DB |
| GET | `/api/vectordb/configs` | ✅ | Listar configs VDB |
| POST | `/api/chat` | ✅ | Chat com AI Agent |
| GET | `/api/audit-log` | admin | Log de auditoria |
| GET | `/api/health` | - | Health check |
---
## 📄 Licença
Projeto interno. Todos os direitos reservados.

655
app.py Normal file
View 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.11.17): 17 controles\n"
"• Networking (2.12.8): 8 controles\n"
"• Compute (3.13.3): 3 controles\n"
"• Logging & Monitoring (4.14.18): 18 controles\n"
"• Storage Object (5.1.15.1.3): 3 controles\n"
"• Storage Block (5.2.15.2.2): 2 controles\n"
"• Storage FSS (5.3.1): 1 controle\n"
"• Asset Management (6.16.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)

169
cis_runner.py Normal file
View File

@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""
CIS Runner Stub — Generates placeholder reports.
Replace this with the actual MCP server integration later.
Usage: python3 cis_runner.py --config /path/to/oci/config --output /path/to/dir --tenancy-name mytenancy [--regions r1,r2]
"""
import argparse, json, datetime, sys
from pathlib import Path
CHECKS = {
"1.1": {"id":"IAM-1","section":"Identity and Access Management","title":"Ensure service level admins are created to manage resources of particular service"},
"1.2": {"id":"IAM-2","section":"Identity and Access Management","title":"Ensure permissions on all resources are given only to the tenancy administrator group"},
"1.3": {"id":"IAM-3","section":"Identity and Access Management","title":"Ensure IAM administrators cannot update tenancy Administrators group"},
"1.4": {"id":"IAM-4","section":"Identity and Access Management","title":"Ensure IAM password policy requires minimum length of 14 or greater"},
"1.5": {"id":"IAM-5","section":"Identity and Access Management","title":"Ensure IAM password policy expires passwords within 365 days"},
"1.6": {"id":"IAM-6","section":"Identity and Access Management","title":"Ensure IAM password policy prevents password reuse"},
"1.7": {"id":"IAM-7","section":"Identity and Access Management","title":"Ensure MFA is enabled for all users with a console password"},
"1.8": {"id":"IAM-8","section":"Identity and Access Management","title":"Ensure user API keys rotate within 90 days"},
"1.9": {"id":"IAM-9","section":"Identity and Access Management","title":"Ensure user customer secret keys rotate within 90 days"},
"1.10": {"id":"IAM-10","section":"Identity and Access Management","title":"Ensure user auth tokens rotate within 90 days"},
"1.11": {"id":"IAM-11","section":"Identity and Access Management","title":"Ensure user IAM Database Passwords rotate within 90 days"},
"1.12": {"id":"IAM-12","section":"Identity and Access Management","title":"Ensure API keys are not created for tenancy administrator users"},
"1.13": {"id":"IAM-13","section":"Identity and Access Management","title":"Ensure all OCI IAM user accounts have a valid and current email address"},
"1.14": {"id":"IAM-14","section":"Identity and Access Management","title":"Ensure Instance Principal authentication is used"},
"1.15": {"id":"IAM-15","section":"Identity and Access Management","title":"Ensure storage service-level admins cannot delete resources they manage"},
"1.16": {"id":"IAM-16","section":"Identity and Access Management","title":"Ensure OCI IAM credentials unused for 45 days or more are disabled"},
"1.17": {"id":"IAM-17","section":"Identity and Access Management","title":"Ensure there is only one active API Key per user"},
"2.1": {"id":"NTW-1","section":"Networking","title":"Ensure no security lists allow ingress from 0.0.0.0/0 to port 22"},
"2.2": {"id":"NTW-2","section":"Networking","title":"Ensure no security lists allow ingress from 0.0.0.0/0 to port 3389"},
"2.3": {"id":"NTW-3","section":"Networking","title":"Ensure no network security groups allow ingress from 0.0.0.0/0 to port 22"},
"2.4": {"id":"NTW-4","section":"Networking","title":"Ensure no network security groups allow ingress from 0.0.0.0/0 to port 3389"},
"2.5": {"id":"NTW-5","section":"Networking","title":"Ensure the default security list of every VCN restricts all traffic except ICMP"},
"2.6": {"id":"NTW-6","section":"Networking","title":"Ensure Oracle Integration Cloud (OIC) access is restricted"},
"2.7": {"id":"NTW-7","section":"Networking","title":"Ensure Oracle Analytics Cloud (OAC) access is restricted or deployed within a VCN"},
"2.8": {"id":"NTW-8","section":"Networking","title":"Ensure Oracle Autonomous Shared Database access is restricted or deployed within a VCN"},
"3.1": {"id":"COM-1","section":"Compute","title":"Ensure Compute Instance Legacy Metadata service endpoint is disabled"},
"3.2": {"id":"COM-2","section":"Compute","title":"Ensure Secure Boot is enabled on Compute Instance"},
"3.3": {"id":"COM-3","section":"Compute","title":"Ensure In-transit Encryption is enabled on Compute Instance"},
"4.1": {"id":"LAM-1","section":"Logging and Monitoring","title":"Ensure default tags are used on resources"},
"4.2": {"id":"LAM-2","section":"Logging and Monitoring","title":"Create at least one notification topic and subscription"},
"4.3": {"id":"LAM-3","section":"Logging and Monitoring","title":"Ensure a notification is configured for Identity Provider changes"},
"4.4": {"id":"LAM-4","section":"Logging and Monitoring","title":"Ensure a notification is configured for IdP group mapping changes"},
"4.5": {"id":"LAM-5","section":"Logging and Monitoring","title":"Ensure a notification is configured for IAM group changes"},
"4.6": {"id":"LAM-6","section":"Logging and Monitoring","title":"Ensure a notification is configured for IAM policy changes"},
"4.7": {"id":"LAM-7","section":"Logging and Monitoring","title":"Ensure a notification is configured for user changes"},
"4.8": {"id":"LAM-8","section":"Logging and Monitoring","title":"Ensure a notification is configured for VCN changes"},
"4.9": {"id":"LAM-9","section":"Logging and Monitoring","title":"Ensure a notification is configured for changes to route tables"},
"4.10": {"id":"LAM-10","section":"Logging and Monitoring","title":"Ensure a notification is configured for security list changes"},
"4.11": {"id":"LAM-11","section":"Logging and Monitoring","title":"Ensure a notification is configured for network security group changes"},
"4.12": {"id":"LAM-12","section":"Logging and Monitoring","title":"Ensure a notification is configured for changes to network gateways"},
"4.13": {"id":"LAM-13","section":"Logging and Monitoring","title":"Ensure VCN flow logging is enabled for all subnets"},
"4.14": {"id":"LAM-14","section":"Logging and Monitoring","title":"Ensure Cloud Guard is enabled in the root compartment"},
"4.15": {"id":"LAM-15","section":"Logging and Monitoring","title":"Ensure a notification is configured for Cloud Guard problems detected"},
"4.16": {"id":"LAM-16","section":"Logging and Monitoring","title":"Ensure customer created CMK is rotated at least annually"},
"4.17": {"id":"LAM-17","section":"Logging and Monitoring","title":"Ensure write level Object Storage logging is enabled for all buckets"},
"4.18": {"id":"LAM-18","section":"Logging and Monitoring","title":"Ensure a notification is configured for Local OCI User Authentication"},
"5.1.1":{"id":"STO-1-1","section":"Storage - Object Storage","title":"Ensure no Object Storage buckets are publicly visible"},
"5.1.2":{"id":"STO-1-2","section":"Storage - Object Storage","title":"Ensure Object Storage Buckets are encrypted with a CMK"},
"5.1.3":{"id":"STO-1-3","section":"Storage - Object Storage","title":"Ensure Versioning is Enabled for Object Storage Buckets"},
"5.2.1":{"id":"STO-2-1","section":"Storage - Block Volumes","title":"Ensure Block Volumes are encrypted with Customer Managed Keys"},
"5.2.2":{"id":"STO-2-2","section":"Storage - Block Volumes","title":"Ensure Boot Volumes are encrypted with Customer Managed Key"},
"5.3.1":{"id":"STO-3-1","section":"Storage - File Storage Service","title":"Ensure File Storage Systems are encrypted with Customer Managed Keys"},
"6.1": {"id":"AM-1","section":"Asset Management","title":"Create at least one compartment in your tenancy"},
"6.2": {"id":"AM-2","section":"Asset Management","title":"Ensure no resources are created in the root compartment"},
}
def gen_html(findings, tenancy, output):
now = datetime.datetime.now()
sections = {}
for cid, ck in findings.items():
sec = ck["section"]
sections.setdefault(sec, {"total":0,"passed":0,"failed":0})
sections[sec]["total"] += 1
if ck["status"] == "PASS": sections[sec]["passed"] += 1
elif ck["status"] == "FAIL": sections[sec]["failed"] += 1
tot = sum(s["total"] for s in sections.values())
pas = sum(s["passed"] for s in sections.values())
fai = sum(s["failed"] for s in sections.values())
sec_rows = "".join(f'<tr><td><strong>{s}</strong></td><td>{v["total"]}</td>'
f'<td style="color:#dc2626;font-weight:700">{v["failed"]}</td>'
f'<td style="color:#16a34a;font-weight:700">{v["passed"]}</td></tr>' for s,v in sorted(sections.items()))
det_rows = ""
for cid in sorted(findings.keys(), key=lambda x: [int(p) if p.isdigit() else p for p in x.replace('.',' ').split()]):
ck = findings[cid]; st = ck["status"]
cls = "pass" if st=="PASS" else "fail" if st=="FAIL" else "review"
det_rows += f'<tr><td>{ck["id"]}</td><td>{cid}</td><td>{ck["title"]}</td><td><span class="b b-{cls}">{st}</span></td><td>{ck["section"]}</td></tr>'
html = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Cloud Security Assessment - {tenancy}</title><style>
*{{margin:0;padding:0;box-sizing:border-box}}body{{font-family:'Segoe UI',system-ui,sans-serif;background:#f8f9fa;color:#1a1a2e;line-height:1.6}}
.c{{max-width:1200px;margin:0 auto;padding:2rem}}h1{{font-size:2.5rem;color:#1a1a2e;margin-bottom:.5rem}}
h2{{color:#2d3748;margin:2rem 0 1rem;padding-bottom:.5rem;border-bottom:2px solid #e2e8f0}}
.m{{color:#718096;margin-bottom:2rem}}.d{{background:#fff;border:1px solid #e2e8f0;border-radius:8px;padding:1.5rem;margin:2rem 0}}
.d h3{{color:#c53030;margin-bottom:.5rem}}.d p{{font-size:.85rem;color:#4a5568}}
table{{width:100%;border-collapse:collapse;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.1);margin:1rem 0}}
th{{background:#1a365d;color:#fff;padding:.75rem 1rem;text-align:left;font-size:.8rem;text-transform:uppercase;letter-spacing:.05em}}
td{{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}}tr:hover{{background:#f7fafc}}
.b{{padding:.25rem .75rem;border-radius:12px;font-size:.8rem;font-weight:600}}
.b-pass{{background:#c6f6d5;color:#22543d}}.b-fail{{background:#fed7d7;color:#9b2c2c}}.b-review{{background:#fefcbf;color:#744210}}
.sc{{display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;margin:1.5rem 0}}
.sd{{background:#fff;border-radius:8px;padding:1.5rem;box-shadow:0 1px 3px rgba(0,0,0,.1);text-align:center}}
.sd .n{{font-size:2.5rem;font-weight:700}}.sd .l{{color:#718096;font-size:.9rem}}
.ft{{text-align:center;color:#a0aec0;margin-top:3rem;padding:1rem;border-top:1px solid #e2e8f0}}
</style></head><body><div class="c">
<h1>Cloud Security Assessment</h1>
<div class="m"><strong>Tenancy:</strong> {tenancy}<br>Tenancy Cloud Infrastructure<br><br>
{now.strftime('%B, %Y')}, Version [1.0]<br>Extract date: {now.isoformat()[:19]}</div>
<div class="d"><h3>Disclaimer</h3>
<p>This document, in any form, software or printed matter, contains proprietary information that is the exclusive property of Oracle.
Your access to and use of this confidential material is subject to the terms and conditions of your Oracle software license and service agreement,
which has been executed and with which you agree to comply.</p>
<p style="margin-top:.5rem">This document is for informational purposes only and is intended solely to assist you in planning for the implementation and
upgrade of the product features described. It is not a commitment to deliver any material, code, or functionality.</p></div>
<h2>Findings Overview</h2><p style="color:#718096">Resumo por domínio (Section)</p>
<div class="sc"><div class="sd"><div class="n" style="color:#2d3748">{tot}</div><div class="l">Total Controls</div></div>
<div class="sd"><div class="n" style="color:#16a34a">{pas}</div><div class="l">Passed</div></div>
<div class="sd"><div class="n" style="color:#dc2626">{fai}</div><div class="l">Failed</div></div></div>
<table><thead><tr><th>Domains</th><th>Total Controls</th><th>Failed</th><th>Passed</th></tr></thead><tbody>{sec_rows}</tbody></table>
<p style="color:#718096;margin-top:.5rem">Total: {tot} · Passed: {pas} · Failed: {fai}</p>
<h2>Detailed Findings</h2>
<table><thead><tr><th>ID</th><th>Check #</th><th>Title</th><th>Status</th><th>Section</th></tr></thead><tbody>{det_rows}</tbody></table>
<div class="ft">Generated by OCI CIS AI Agent · CIS OCI Foundations Benchmark 3.0 · {now.strftime('%Y-%m-%d %H:%M:%S')}</div>
</div></body></html>"""
Path(output).write_text(html, encoding="utf-8")
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--config", required=True)
ap.add_argument("--output", required=True)
ap.add_argument("--tenancy-name", default="Tenancy")
ap.add_argument("--regions", default=None)
args = ap.parse_args()
out = Path(args.output); out.mkdir(parents=True, exist_ok=True)
# TODO: Replace with MCP server call
# For now generate a stub report showing all checks as REVIEW
print(f"[CIS Runner] Config: {args.config}")
print(f"[CIS Runner] Output: {args.output}")
print(f"[CIS Runner] Tenancy: {args.tenancy_name}")
# Check if OCI config file exists
if not Path(args.config).exists():
print(f"[ERROR] Config not found: {args.config}", file=sys.stderr)
sys.exit(1)
findings = {}
for cid, ck in CHECKS.items():
findings[cid] = {**ck, "status": "REVIEW", "findings": [], "total": []}
report = {
"tenancy": args.tenancy_name,
"generated_at": datetime.datetime.now().isoformat(),
"benchmark": "CIS OCI Foundations Benchmark 3.0",
"regions": args.regions.split(",") if args.regions else ["all"],
"summary": {
"total": len(findings),
"passed": sum(1 for f in findings.values() if f["status"]=="PASS"),
"failed": sum(1 for f in findings.values() if f["status"]=="FAIL"),
"review": sum(1 for f in findings.values() if f["status"]=="REVIEW"),
},
"findings": findings,
}
(out / "report.json").write_text(json.dumps(report, indent=2, default=str))
gen_html(findings, args.tenancy_name, str(out / "report.html"))
print("[CIS Runner] Reports generated successfully")
if __name__ == "__main__":
main()

22
default.conf Normal file
View File

@@ -0,0 +1,22 @@
server {
listen 80;
server_name _;
client_max_body_size 50M;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_connect_timeout 60s;
}
}

View File

@@ -14,7 +14,7 @@ services:
volumes: volumes:
- agent-data:/data - agent-data:/data
ports: ports:
- "8000:8000" - "8000:8000" # Optional: direct API access for debugging
networks: networks:
- agent-net - agent-net
healthcheck: healthcheck:

360
index.html Normal file
View File

@@ -0,0 +1,360 @@
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>OCI CIS AI Agent</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{--bg0:#0a0e1a;--bg1:#111827;--bg2:#1a2035;--bg3:#0d1321;--bd:#1e293b;--bf:#f97316;
--t1:#e2e8f0;--t2:#94a3b8;--tm:#64748b;--ac:#f97316;--ah:#ea580c;
--gn:#22c55e;--rd:#ef4444;--bl:#3b82f6;--yl:#eab308;--pp:#8b5cf6;
--r:8px;--rl:12px;--sh:0 4px 24px rgba(0,0,0,.3);--fm:'JetBrains Mono',monospace;--fs:'DM Sans',sans-serif}
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:var(--fs);background:var(--bg0);color:var(--t1);min-height:100vh}
.app{display:flex;min-height:100vh}
.sb{width:250px;background:var(--bg1);border-right:1px solid var(--bd);display:flex;flex-direction:column;position:fixed;top:0;left:0;bottom:0;z-index:100}
.sb-h{padding:1.25rem;border-bottom:1px solid var(--bd)}
.sb-h h1{font-size:.95rem;font-weight:700;color:var(--ac);font-family:var(--fm);letter-spacing:-.02em}
.sb-h .st{font-size:.65rem;color:var(--tm);margin-top:2px;font-family:var(--fm)}
.nav{padding:.75rem .6rem;flex:1;overflow-y:auto}
.nl{font-size:.6rem;color:var(--tm);text-transform:uppercase;letter-spacing:.1em;padding:.5rem .6rem;font-weight:600}
.ni{display:flex;align-items:center;gap:.65rem;padding:.55rem .6rem;border-radius:var(--r);cursor:pointer;color:var(--t2);font-size:.82rem;font-weight:500;transition:all .15s;margin-bottom:1px}
.ni:hover{background:rgba(249,115,22,.08);color:var(--t1)}.ni.on{background:rgba(249,115,22,.15);color:var(--ac)}
.ni .ic{width:16px;text-align:center;font-size:.95rem}
.sb-f{padding:.75rem 1rem;border-top:1px solid var(--bd)}
.ui{display:flex;align-items:center;gap:.6rem}
.ua{width:30px;height:30px;background:var(--ac);border-radius:50%;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:.75rem;color:#fff}
.un{font-weight:600;color:var(--t1);font-size:.8rem}.ur{color:var(--tm);font-size:.65rem;font-family:var(--fm)}
.mc{flex:1;margin-left:250px;min-height:100vh}
.tb{height:52px;background:var(--bg1);border-bottom:1px solid var(--bd);display:flex;align-items:center;justify-content:space-between;padding:0 1.25rem;position:sticky;top:0;z-index:50}
.tb-t{font-weight:600;font-size:.9rem}.tb-a{display:flex;align-items:center;gap:.6rem}
.pc{padding:1.25rem}
.cd{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl);padding:1.25rem;margin-bottom:.75rem}
.ct{font-weight:600;font-size:.9rem;margin-bottom:.75rem;display:flex;align-items:center;gap:.4rem}
.btn{display:inline-flex;align-items:center;gap:.4rem;padding:.45rem .85rem;border-radius:var(--r);font-size:.78rem;font-weight:600;cursor:pointer;border:1px solid transparent;transition:all .15s;font-family:var(--fs)}
.bp{background:var(--ac);color:#fff;border-color:var(--ac)}.bp:hover{background:var(--ah)}
.bs{background:transparent;color:var(--t2);border-color:var(--bd)}.bs:hover{border-color:var(--tm);color:var(--t1)}
.bd{background:var(--rd);color:#fff}.bd:hover{background:#dc2626}
.bsm{padding:.3rem .6rem;font-size:.72rem}
.btn:disabled{opacity:.5;cursor:not-allowed}
.ig{margin-bottom:.75rem}.ig label{display:block;font-size:.78rem;font-weight:600;color:var(--t2);margin-bottom:.25rem}
.ig .ht{font-size:.67rem;color:var(--tm);margin-bottom:.2rem}
input[type=text],input[type=password],input[type=email],input[type=number],select,textarea{width:100%;padding:.5rem .65rem;background:var(--bg3);border:1px solid var(--bd);border-radius:var(--r);color:var(--t1);font-size:.8rem;font-family:var(--fm);transition:border .15s}
input:focus,select:focus,textarea:focus{outline:none;border-color:var(--ac)}
input[type=file]{font-family:var(--fm);font-size:.75rem;color:var(--t2)}
.badge{padding:.18rem .55rem;border-radius:12px;font-size:.68rem;font-weight:600;font-family:var(--fm)}
.b-pass{background:rgba(34,197,94,.15);color:var(--gn)}.b-fail{background:rgba(239,68,68,.15);color:var(--rd)}
.b-pend{background:rgba(234,179,8,.15);color:var(--yl)}.b-rev{background:rgba(59,130,246,.15);color:var(--bl)}
.b-admin{background:rgba(139,92,246,.15);color:var(--pp)}.b-user{background:rgba(59,130,246,.15);color:var(--bl)}
.b-viewer{background:rgba(148,163,184,.15);color:var(--tm)}
table{width:100%;border-collapse:collapse;font-size:.78rem}
th{text-align:left;padding:.55rem .65rem;font-size:.67rem;text-transform:uppercase;letter-spacing:.05em;color:var(--tm);border-bottom:1px solid var(--bd);font-weight:600}
td{padding:.55rem .65rem;border-bottom:1px solid rgba(30,41,59,.4);color:var(--t2)}
tr:hover td{background:rgba(249,115,22,.03)}
.g2{display:grid;grid-template-columns:1fr 1fr;gap:.75rem}
.g3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.75rem}
.ch-c{display:flex;flex-direction:column;height:calc(100vh - 52px - 2.5rem)}
.ch-m{flex:1;overflow-y:auto;padding:.75rem;background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl) var(--rl) 0 0}
.cm{margin-bottom:.75rem;display:flex;gap:.6rem}
.cm-u{justify-content:flex-end}
.cm-u .cb{background:var(--ac);color:#fff;border-radius:var(--rl) var(--rl) 4px var(--rl);border-color:var(--ac)}
.cb{max-width:75%;padding:.6rem .85rem;border-radius:var(--rl) var(--rl) var(--rl) 4px;background:var(--bg1);font-size:.82rem;line-height:1.5;border:1px solid var(--bd);white-space:pre-wrap}
.cb code{font-family:var(--fm);font-size:.75rem;background:rgba(0,0,0,.3);padding:.1rem .25rem;border-radius:3px}
.cb strong{color:var(--ac)}
.ch-i{display:flex;gap:.4rem;padding:.6rem;background:var(--bg1);border:1px solid var(--bd);border-top:none;border-radius:0 0 var(--rl) var(--rl)}
.ch-i input{flex:1;background:var(--bg3)}
.rif{width:100%;height:calc(100vh - 52px - 5.5rem);border:1px solid var(--bd);border-radius:var(--rl);background:#fff}
.lp{min-height:100vh;display:flex;align-items:center;justify-content:center;background:var(--bg0);background-image:radial-gradient(ellipse at 20% 50%,rgba(249,115,22,.08) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(139,92,246,.06) 0%,transparent 50%)}
.lc{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--rl);padding:2.25rem;width:100%;max-width:380px;box-shadow:var(--sh)}
.lc h2{font-size:1.15rem;font-family:var(--fm);color:var(--ac)}.lc .st{color:var(--tm);font-size:.78rem;margin-bottom:1.25rem}
.al{padding:.6rem .85rem;border-radius:var(--r);font-size:.78rem;margin-bottom:.75rem}
.al-e{background:rgba(239,68,68,.1);color:var(--rd);border:1px solid rgba(239,68,68,.2)}
.al-s{background:rgba(34,197,94,.1);color:var(--gn);border:1px solid rgba(34,197,94,.2)}
.al-i{background:rgba(59,130,246,.1);color:var(--bl);border:1px solid rgba(59,130,246,.2)}
.tag{display:inline-block;padding:.12rem .4rem;background:rgba(249,115,22,.1);color:var(--ac);border-radius:4px;font-size:.67rem;font-family:var(--fm)}
.emp{text-align:center;padding:2.5rem;color:var(--tm)}.emp .eic{font-size:2.2rem;margin-bottom:.5rem}
@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.fi{animation:fi .25s ease}
@keyframes pu{0%,100%{opacity:1}50%{opacity:.5}}.ld{animation:pu 1.5s infinite;color:var(--tm);font-size:.82rem;text-align:center;padding:1.5rem}
::-webkit-scrollbar{width:5px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--bd);border-radius:3px}
@media(max-width:768px){.sb{transform:translateX(-100%)}.sb.open{transform:translateX(0)}.mc{margin-left:0}.g2,.g3{grid-template-columns:1fr}}
</style>
</head>
<body>
<div id="app"></div>
<script>
const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],vdbCfg:[],dls:[],users:[],auditLogs:[]};
const API='/api';
async function $api(p,o={}){
const h={...(o.headers||{})};
if(S.token)h['Authorization']='Bearer '+S.token;
if(o.body&&!(o.body instanceof FormData)){h['Content-Type']='application/json';o.body=JSON.stringify(o.body)}
const r=await fetch(API+p,{...o,headers:h});
if(r.status===401){S.user=null;S.token=null;localStorage.removeItem('t');R();throw new Error('Unauthorized')}
const d=await r.json();if(!r.ok)throw new Error(d.detail||d.message||'Erro');return d;
}
async function doLogin(u,pw,totp){
const b={username:u,password:pw};if(totp)b.totp_code=totp;
const d=await $api('/auth/login',{method:'POST',body:b});
if(d.mfa_required)return d;
S.token=d.token;S.user=d.user;localStorage.setItem('t',d.token);await loadData();R();return d;
}
async function doLogout(){try{await $api('/auth/logout',{method:'POST'})}catch(e){}S.user=null;S.token=null;S.msgs=[];localStorage.removeItem('t');R()}
async function checkAuth(){const t=localStorage.getItem('t');if(!t)return;S.token=t;try{S.user=await $api('/users/me');await loadData()}catch(e){S.token=null;localStorage.removeItem('t')}}
async function loadData(){
try{[S.ociCfg,S.reports]=await Promise.all([$api('/oci/configs'),$api('/reports')]);
if(S.user.role==='admin'){S.users=await $api('/users')}
try{S.vdbCfg=await $api('/vectordb/configs')}catch(e){S.vdbCfg=[]}
}catch(e){console.error(e)}
}
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind()}
function switchTab(t){S.tab=t;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl()}
function rLogin(){return`
<div class="lp"><div class="lc fi">
<div style="text-align:center;margin-bottom:1.25rem"><div style="font-size:1.8rem;margin-bottom:.4rem">🛡️</div>
<h2>OCI CIS Agent</h2><div class="st">Infrastructure & Security Engineer</div></div>
<div id="le"></div>
<div id="mfa-s" style="display:none"><div class="al al-i">MFA obrigatório. Insira o código do autenticador.</div>
<div class="ig"><label>Código MFA</label><input type="text" id="mfa-c" placeholder="000000" maxlength="6" style="text-align:center;font-size:1.1rem;letter-spacing:.3em"></div>
<button class="btn bp" style="width:100%" onclick="hMfa()">Verificar</button></div>
<div id="lf"><div class="ig"><label>Usuário</label><input type="text" id="lu" placeholder="admin" autofocus></div>
<div class="ig"><label>Senha</label><input type="password" id="lp" placeholder="••••••••" onkeydown="if(event.key==='Enter')hLogin()"></div>
<button class="btn bp" style="width:100%" onclick="hLogin()">Entrar</button>
<div style="text-align:center;margin-top:.75rem;font-size:.67rem;color:var(--tm)">Padrão: admin / admin123</div></div></div></div>`}
function rApp(){return`<div class="app">${rSb()}<div class="mc">${rTb()}<div class="pc fi" id="pg">${rPg()}</div></div></div>`}
function rSb(){
const tabs=[['chat','💬','Chat Agent'],['report','📊','Report'],['downloads','📁','Downloads'],['oci-config','☁️','Config OCI'],['vectordb','🔗','Vector DB']];
const atabs=[['users','👥','Usuários'],['mfa','🔐','MFA'],['audit','📋','Audit Log']];
const i=(S.user?.username||'?')[0].toUpperCase();
return`<div class="sb"><div class="sb-h"><h1>🛡️ OCI CIS Agent</h1><div class="st">Infrastructure & Security</div></div>
<div class="nav"><div class="nl">Principal</div>
${tabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}
${S.user?.role==='admin'?`<div class="nl" style="margin-top:.75rem">Administração</div>
${atabs.map(t=>`<div class="ni ${S.tab===t[0]?'on':''}" onclick="switchTab('${t[0]}')"><span class="ic">${t[1]}</span>${t[2]}</div>`).join('')}`:''}</div>
<div class="sb-f"><div class="ui"><div class="ua">${i}</div><div><div class="un">${S.user?.username}</div><div class="ur">${S.user?.role}</div></div>
<button class="btn bs bsm" style="margin-left:auto" onclick="doLogout()" title="Sair">⏻</button></div></div></div>`}
function rTb(){
const t={'chat':'💬 AI Agent Chat','report':'📊 Compliance Report','downloads':'📁 Downloads','oci-config':'☁️ Configuração OCI','vectordb':'🔗 Vector Database','users':'👥 Gerenciar Usuários','mfa':'🔐 Autenticação MFA','audit':'📋 Audit Log'};
return`<div class="tb"><div class="tb-t">${t[S.tab]||''}</div><div class="tb-a"><span class="tag">session: ${(S.sid||'---').slice(0,8)}</span></div></div>`}
function rPg(){switch(S.tab){case'chat':return rChat();case'report':return rReport();case'downloads':return rDl();case'oci-config':return rOci();case'vectordb':return rVdb();case'users':return rUsers();case'mfa':return rMfa();case'audit':return rAudit();default:return''}}
// ── Chat ─────────────────────────────────────────────────────────────────
function rChat(){
const ms=S.msgs.length===0?'<div class="emp"><div class="eic">🤖</div><p>Envie uma mensagem para iniciar.</p></div>'
:S.msgs.map(m=>`<div class="cm cm-${m.r}"><div class="cb">${fm(m.c)}</div></div>`).join('');
return`<div class="ch-c">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.4rem">
<span style="font-size:.78rem;color:var(--tm)">Pronto.</span>
<button class="btn bd bsm" onclick="clrChat()">Limpar conversa</button></div>
<div class="ch-m" id="chm">${ms}</div>
<div class="ch-i"><input type="text" id="chi" placeholder="Digite sua mensagem..." onkeydown="if(event.key==='Enter')sChat()">
<button class="btn bp" onclick="sChat()">Enviar</button></div></div>`}
function fm(t){return t.replace(/\*\*(.*?)\*\*/g,'<strong>$1</strong>').replace(/`(.*?)`/g,'<code>$1</code>').replace(/\n/g,'<br>')}
async function sChat(){const el=document.getElementById('chi');const m=el.value.trim();if(!m)return;el.value='';
S.msgs.push({r:'user',c:m});R();scCh();
try{const d=await $api('/chat',{method:'POST',body:{message:m,session_id:S.sid}});S.sid=d.session_id;S.msgs.push({r:'assistant',c:d.response});R();scCh()}
catch(e){S.msgs.push({r:'assistant',c:'❌ Erro: '+e.message});R()}}
function scCh(){setTimeout(()=>{const e=document.getElementById('chm');if(e)e.scrollTop=e.scrollHeight},50)}
async function clrChat(){if(S.sid)try{await $api('/chat/'+S.sid,{method:'DELETE'})}catch(e){}S.msgs=[];S.sid=null;R()}
// ── Report ───────────────────────────────────────────────────────────────
function rReport(){
const comp=S.reports.filter(r=>r.status==='completed');
if(!comp.length)return`<div class="cd"><div class="ct">📊 Report (HTML)</div>
<div class="emp"><div class="eic">📄</div><p>Nenhum relatório disponível.</p>
<p style="font-size:.72rem;margin-top:.4rem">Configure uma conexão OCI e execute um relatório.</p></div></div>${rRunRpt()}`;
const lt=comp[0];
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem">
<span style="font-size:.78rem;color:var(--tm)">Report (HTML) — ${lt.tenancy_name}</span>
<div style="display:flex;gap:.4rem"><select id="rsel" onchange="ldRpt(this.value)" style="max-width:280px">
${comp.map(r=>`<option value="${r.id}">${r.tenancy_name}${r.created_at}</option>`).join('')}</select>
<button class="btn bs bsm" onclick="ldRpt(document.getElementById('rsel').value)">Recarregar</button></div></div>
<iframe class="rif" id="rfr" src="${API}/reports/${lt.id}/html"></iframe>${rRunRpt()}`}
function rRunRpt(){if(S.user?.role==='viewer')return'';
return`<div class="cd" style="margin-top:.75rem"><div class="ct">🚀 Executar Novo Relatório</div>
<div class="g2"><div class="ig"><label>Configuração OCI</label>
<select id="rc"><option value="">Selecione...</option>${S.ociCfg.map(c=>`<option value="${c.id}">${c.tenancy_name} (${c.region})</option>`).join('')}</select></div>
<div class="ig"><label>Regiões (opcional)</label><input type="text" id="rr" placeholder="sa-saopaulo-1, us-ashburn-1"></div></div>
<button class="btn bp" onclick="runRpt()">▶ Executar CIS Compliance</button></div>`}
function ldRpt(id){const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html'}
async function runRpt(){const c=document.getElementById('rc').value;if(!c)return alert('Selecione uma configuração OCI');
const rg=document.getElementById('rr').value.split(',').map(r=>r.trim()).filter(Boolean);
try{const d=await $api('/reports/run',{method:'POST',body:{config_id:c,regions:rg.length?rg:null}});
alert('Relatório iniciado! ID: '+d.report_id);S.reports=await $api('/reports');R()}catch(e){alert('Erro: '+e.message)}}
// ── Downloads ────────────────────────────────────────────────────────────
function rDl(){return`
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.5rem">
<span style="font-size:.78rem;color:var(--tm)">${S.reports.length} relatório(s)</span>
<div style="display:flex;gap:.4rem"><input type="text" id="dlf" placeholder="Filtrar por tenancy name ou nome do arquivo..." style="max-width:320px" oninput="fDl()">
<button class="btn bp bsm" onclick="refreshDl()">🔄 Atualizar lista</button></div></div>
<div class="cd"><table><thead><tr><th>Tenancy</th><th>Status</th><th>Criado</th><th>Concluído</th><th>Ações</th></tr></thead>
<tbody id="dlt">${S.reports.map(r=>`<tr data-n="${r.tenancy_name.toLowerCase()}">
<td><strong>${r.tenancy_name}</strong></td>
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'?'b-fail':'b-pend'}">${r.status}</span></td>
<td>${r.created_at||'—'}</td><td>${r.completed_at||'—'}</td>
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button>
<button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button>`:'—'}</td></tr>`).join('')}</tbody></table>
${!S.reports.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório encontrado.</p></div>':''}</div>`}
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
async function refreshDl(){S.reports=await $api('/reports');R()}
function fDl(){const q=document.getElementById('dlf').value.toLowerCase();document.querySelectorAll('#dlt tr').forEach(tr=>{tr.style.display=tr.dataset.n?.includes(q)?'':'none'})}
// ── OCI Config ───────────────────────────────────────────────────────────
function rOci(){return`
<div class="cd"><div class="ct">☁️ Configurações OCI Existentes</div>
<table><thead><tr><th>Tenancy</th><th>Region</th><th>OCID</th><th>Criado</th><th>Ações</th></tr></thead><tbody>
${S.ociCfg.map(c=>`<tr><td><strong>${c.tenancy_name}</strong></td><td><span class="tag">${c.region}</span></td>
<td style="font-family:var(--fm);font-size:.67rem">${(c.tenancy_ocid||'').slice(0,28)}...</td><td>${c.created_at}</td>
<td><button class="btn bs bsm" onclick="tOci('${c.id}')">Testar</button>
<button class="btn bd bsm" onclick="dOci('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.ociCfg.length?'<div class="emp" style="padding:.75rem"><p>Nenhuma configuração.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Configuração OCI</div>
<p style="font-size:.78rem;color:var(--tm);margin-bottom:.75rem">Configure as credenciais OCI</p><div id="om"></div>
<div class="g2">
<div class="ig"><label>Tenancy Name</label><div class="ht">Nome identificador do tenancy (usado para nomear os arquivos)</div><input type="text" id="otn" placeholder="minha-empresa"></div>
<div class="ig"><label>OCID User</label><div class="ht">Ex: ocid1.user.oc1..aaaaaaaaxxxxxxxxxx</div><input type="text" id="ouo" placeholder="ocid1.user.oc1..aaaaaa..."></div>
<div class="ig"><label>Fingerprint</label><div class="ht">Ex: aa:bb:cc:dd:ee:ff:00:11:22:33:44:55:66:77:88:99</div><input type="text" id="ofp" placeholder="aa:bb:cc:dd:..."></div>
<div class="ig"><label>OCID Tenancy</label><div class="ht">Ex: ocid1.tenancy.oc1..aaaaaaaaxxxxxxxxxx</div><input type="text" id="oto" placeholder="ocid1.tenancy.oc1..aaaaaa..."></div>
<div class="ig"><label>Region</label><div class="ht">Ex: us-ashburn-1, sa-saopaulo-1, etc.</div><input type="text" id="org" placeholder="sa-saopaulo-1"></div></div>
<div class="g2"><div class="ig"><label>Public Key (.pem)</label><div class="ht">Selecione o arquivo da chave pública</div><input type="file" id="opk" accept=".pem"></div>
<div class="ig"><label>Private Key (.pem)</label><div class="ht">Selecione o arquivo da chave privada</div><input type="file" id="osk" accept=".pem"></div></div>
<button class="btn bp" onclick="sOci()">🚀 Salvar Configuração</button></div>
${S.user?.role==='admin'?`<div class="cd"><div class="ct">🔧 OCI CLI</div>
<button class="btn bs" onclick="iCli()">Instalar OCI CLI no Container</button></div>`:''}`}
async function sOci(){
const fd=new FormData();
fd.append('tenancy_name',document.getElementById('otn').value);
fd.append('tenancy_ocid',document.getElementById('oto').value);
fd.append('user_ocid',document.getElementById('ouo').value);
fd.append('fingerprint',document.getElementById('ofp').value);
fd.append('region',document.getElementById('org').value);
const sk=document.getElementById('osk').files[0];if(!sk)return sm('om','Selecione a chave privada','e');
fd.append('private_key',sk);
const pk=document.getElementById('opk').files[0];if(pk)fd.append('public_key',pk);
try{await $api('/oci/config',{method:'POST',body:fd,headers:{}});S.ociCfg=await $api('/oci/configs');sm('om','✅ Configuração salva!','s');R()}catch(e){sm('om',e.message,'e')}}
async function tOci(id){try{const d=await $api('/oci/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ Conexão OK!':'❌ '+d.message)}catch(e){alert('Erro: '+e.message)}}
async function dOci(id){if(!confirm('Excluir?'))return;await $api('/oci/configs/'+id,{method:'DELETE'});S.ociCfg=await $api('/oci/configs');R()}
async function iCli(){if(!confirm('Instalar OCI CLI? Pode levar alguns minutos.'))return;try{const d=await $api('/oci/install-cli',{method:'POST'});alert(d.status==='success'?'✅ Instalado!':'❌ '+d.message)}catch(e){alert(e.message)}}
// ── Vector DB ────────────────────────────────────────────────────────────
function rVdb(){return`
<div class="cd"><div class="ct">🔗 Conexões Vector Database</div>
<table><thead><tr><th>Tipo</th><th>Host</th><th>Porta</th><th>Database</th><th>Collection</th><th>Ações</th></tr></thead><tbody>
${S.vdbCfg.map(c=>`<tr><td><span class="tag">${c.db_type}</span></td><td>${c.host}</td><td>${c.port}</td>
<td>${c.database_name||'—'}</td><td>${c.collection_name||'—'}</td>
<td><button class="btn bs bsm" onclick="tVdb('${c.id}')">Testar</button>
<button class="btn bd bsm" onclick="dVdb('${c.id}')">Excluir</button></td></tr>`).join('')}</tbody></table>
${!S.vdbCfg.length?'<div class="emp" style="padding:.75rem"><p>Nenhuma conexão configurada.</p></div>':''}</div>
<div class="cd"><div class="ct"> Nova Conexão Vector DB</div><div id="vm"></div>
<div class="g2">
<div class="ig"><label>Tipo de Banco</label><select id="vt"><option value="chromadb">ChromaDB</option><option value="pinecone">Pinecone</option><option value="weaviate">Weaviate</option><option value="pgvector">PGVector (PostgreSQL)</option><option value="qdrant">Qdrant</option><option value="milvus">Milvus</option><option value="oracle23ai">Oracle 23ai Vector</option></select></div>
<div class="ig"><label>Host</label><input type="text" id="vh" placeholder="localhost ou URL"></div>
<div class="ig"><label>Porta</label><input type="number" id="vp" placeholder="8000"></div>
<div class="ig"><label>Database Name</label><input type="text" id="vd" placeholder="(opcional)"></div>
<div class="ig"><label>Collection / Index</label><input type="text" id="vc" placeholder="cis_findings"></div>
<div class="ig"><label>Usuário</label><input type="text" id="vu" placeholder="(opcional)"></div>
<div class="ig"><label>Senha</label><input type="password" id="vw" placeholder="(opcional)"></div>
<div class="ig"><label>API Key</label><input type="password" id="va" placeholder="(opcional)"></div></div>
<button class="btn bp" onclick="sVdb()">💾 Salvar Conexão</button></div>`}
async function sVdb(){try{await $api('/vectordb/config',{method:'POST',body:{
db_type:document.getElementById('vt').value,host:document.getElementById('vh').value,
port:parseInt(document.getElementById('vp').value),
database_name:document.getElementById('vd').value||null,collection_name:document.getElementById('vc').value||null,
username:document.getElementById('vu').value||null,password:document.getElementById('vw').value||null,
api_key:document.getElementById('va').value||null}});S.vdbCfg=await $api('/vectordb/configs');sm('vm','✅ Salvo!','s');R()}catch(e){sm('vm',e.message,'e')}}
async function tVdb(id){try{const d=await $api('/vectordb/test/'+id,{method:'POST'});alert(d.status==='success'?'✅ '+d.message:'❌ '+d.message)}catch(e){alert(e.message)}}
async function dVdb(id){if(!confirm('Excluir?'))return;await $api('/vectordb/configs/'+id,{method:'DELETE'});S.vdbCfg=await $api('/vectordb/configs');R()}
// ── Users ────────────────────────────────────────────────────────────────
function rUsers(){return`
<div class="cd"><div class="ct">👥 Usuários do Sistema</div>
<table><thead><tr><th>Usuário</th><th>Email</th><th>Role</th><th>MFA</th><th>Status</th><th>Último Login</th><th>Ações</th></tr></thead><tbody>
${S.users.map(u=>`<tr><td><strong>${u.username}</strong></td><td>${u.email||'—'}</td>
<td><span class="badge b-${u.role}">${u.role}</span></td><td>${u.mfa_enabled?'✅':'❌'}</td>
<td><span class="badge ${u.is_active?'b-pass':'b-fail'}">${u.is_active?'Ativo':'Inativo'}</span></td>
<td style="font-size:.72rem">${u.last_login||'Nunca'}</td>
<td><select onchange="uRole('${u.id}',this.value)" style="max-width:90px;padding:.2rem">
<option value="admin"${u.role==='admin'?' selected':''}>Admin</option>
<option value="user"${u.role==='user'?' selected':''}>User</option>
<option value="viewer"${u.role==='viewer'?' selected':''}>Viewer</option></select>
${u.is_active?` <button class="btn bd bsm" onclick="deact('${u.id}')">Desativar</button>`:''}</td></tr>`).join('')}</tbody></table></div>
<div class="cd"><div class="ct"> Criar Usuário</div><div id="um"></div>
<div class="g3"><div class="ig"><label>Usuário</label><input type="text" id="nu"></div>
<div class="ig"><label>Email</label><input type="email" id="ne"></div>
<div class="ig"><label>Senha</label><input type="password" id="np"></div></div>
<div class="ig" style="max-width:180px"><label>Role</label><select id="nr"><option value="viewer">Viewer</option><option value="user">User</option><option value="admin">Admin</option></select></div>
<button class="btn bp" onclick="cUser()">Criar Usuário</button></div>`}
async function cUser(){try{await $api('/auth/register',{method:'POST',body:{
username:document.getElementById('nu').value,email:document.getElementById('ne').value,
password:document.getElementById('np').value,role:document.getElementById('nr').value}});
S.users=await $api('/users');sm('um','✅ Usuário criado!','s');R()}catch(e){sm('um',e.message,'e')}}
async function uRole(id,r){try{await $api('/users/'+id,{method:'PUT',body:{role:r}});S.users=await $api('/users')}catch(e){alert(e.message)}}
async function deact(id){if(!confirm('Desativar?'))return;await $api('/users/'+id,{method:'DELETE'});S.users=await $api('/users');R()}
// ── MFA ──────────────────────────────────────────────────────────────────
function rMfa(){return`
<div class="cd"><div class="ct">🔐 Configurar MFA (TOTP)</div>
<p style="font-size:.82rem;color:var(--t2);margin-bottom:.75rem">A autenticação multi-fator adiciona uma camada extra de segurança. Use Google Authenticator ou Authy.</p>
<div id="mm"></div>
${S.user?.mfa_enabled?'<div class="al al-s">✅ MFA está ativado para sua conta.</div>'
:`<button class="btn bp" onclick="sMfa()">Gerar Secret</button><div id="msa" style="margin-top:.75rem"></div>`}</div>
<div class="cd"><div class="ct"> Sobre Roles</div>
<table><thead><tr><th>Role</th><th>Permissões</th></tr></thead><tbody>
<tr><td><span class="badge b-admin">admin</span></td><td>Acesso total: gerenciar usuários, configs, relatórios, MFA, audit log</td></tr>
<tr><td><span class="badge b-user">user</span></td><td>Criar/gerenciar configs OCI e Vector DB, executar relatórios, chat</td></tr>
<tr><td><span class="badge b-viewer">viewer</span></td><td>Visualizar relatórios e downloads, usar chat (somente leitura)</td></tr></tbody></table></div>`}
async function sMfa(){try{const d=await $api('/mfa/setup',{method:'POST'});
document.getElementById('msa').innerHTML=`<div class="al al-i"><strong>Secret:</strong> <code>${d.secret}</code><br>
<strong>URI:</strong> <code style="font-size:.67rem;word-break:break-all">${d.uri}</code></div>
<p style="font-size:.78rem;margin:.6rem 0">Adicione o secret no app autenticador e insira o código:</p>
<div style="display:flex;gap:.4rem;max-width:280px"><input type="text" id="mvc" placeholder="000000" maxlength="6">
<button class="btn bp" onclick="vMfa()">Ativar</button></div>`}catch(e){sm('mm',e.message,'e')}}
async function vMfa(){try{await $api('/mfa/verify',{method:'POST',body:{totp_code:document.getElementById('mvc').value}});
S.user.mfa_enabled=true;sm('mm','✅ MFA ativado!','s');R()}catch(e){sm('mm',e.message,'e')}}
// ── Audit ────────────────────────────────────────────────────────────────
function rAudit(){return`<div class="cd"><div class="ct">📋 Log de Auditoria</div><div id="ac"><div class="ld">Carregando...</div></div></div>`}
async function loadAudit(){try{const logs=await $api('/audit-log?limit=50');
document.getElementById('ac').innerHTML=`<table><thead><tr><th>Data</th><th>Usuário</th><th>Ação</th><th>Recurso</th><th>IP</th></tr></thead><tbody>
${logs.map(l=>`<tr><td style="font-size:.67rem">${l.created_at}</td><td>${l.username||'—'}</td><td><span class="tag">${l.action}</span></td>
<td style="font-size:.67rem">${l.resource?(l.resource.slice(0,12)+'...'):'—'}</td><td>${l.ip||'—'}</td></tr>`).join('')}</tbody></table>`}catch(e){document.getElementById('ac').innerHTML='<div class="al al-e">Erro: '+e.message+'</div>'}}
// ── Helpers ──────────────────────────────────────────────────────────────
function sm(id,msg,t){const el=document.getElementById(id);if(el)el.innerHTML=`<div class="al al-${t==='s'?'s':t==='i'?'i':'e'}">${msg}</div>`}
let _lu='',_lp='';
async function hLogin(){const u=document.getElementById('lu').value,p=document.getElementById('lp').value;
_lu=u;_lp=p;try{const d=await doLogin(u,p);
if(d&&d.mfa_required){document.getElementById('lf').style.display='none';document.getElementById('mfa-s').style.display='block'}}
catch(e){document.getElementById('le').innerHTML=`<div class="al al-e">${e.message}</div>`}}
async function hMfa(){try{await doLogin(_lu,_lp,document.getElementById('mfa-c').value)}
catch(e){document.getElementById('le').innerHTML=`<div class="al al-e">${e.message}</div>`}}
function bind(){/* re-bind any handlers after render if needed */}
// ── Init ─────────────────────────────────────────────────────────────────
(async()=>{await checkAuth();R()})();
</script>
</body>
</html>

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
fastapi==0.115.0
uvicorn[standard]==0.30.0
python-multipart==0.0.12
PyJWT==2.9.0
python-dotenv==1.0.1