From 1b3c02d10bbae6b91987cdcec41f2c317d60ebae Mon Sep 17 00:00:00 2001
From: nogueiraguh
Date: Tue, 31 Mar 2026 10:44:13 -0300
Subject: [PATCH] =?UTF-8?q?feat:=20v3.0=20=E2=80=94=20CIS=20PDF=20chunker,?=
=?UTF-8?q?=20DOCX=20professional,=20RAG=20complete,=20progress=20bar,=20c?=
=?UTF-8?q?ode=20cleanup?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- CIS PDF chunker: segments by recommendation (54/54 complete with desc+rationale+rem), 7000 chars target, 500 overlap
- RAG: 60s timeout + retry, direct SQL fetch by recommendationNumber, concatenate all chunks
- Auto-detect embedding dimension from DDL (empty tables), auto-detect model per dimension
- DOCX: camo strip (Pillow), A4 centered cover, code blocks (Consolas), lists with hanging indent
- Compliance report: progress bar (RAG X/35 percentage), Audit/Verification bold
- Code cleanup: removed redundant imports (io, base64, time, Response, HTMLResponse), duplicate DB query
- EmbeddingsPage: auto-select ADB config fix
- ReportsPage: removed DOCX preview (docx-preview lib)
- README updated to v3.0 with all new features
---
README.md | 232 ++---
backend/app.py | 852 ++++++++++++++----
frontend-react/src/pages/ReportsPage.tsx | 41 +-
.../src/pages/config/EmbeddingsPage.tsx | 11 +
4 files changed, 852 insertions(+), 284 deletions(-)
diff --git a/README.md b/README.md
index 9518107..c9ff818 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -29,67 +29,57 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
## Features
-### π€ AI Chat Agent with RAG + MCP Tool Use
+### AI Chat Agent with RAG + MCP Tool Use
- **OCI Generative AI** integration via official SDK (`oci.generative_ai_inference`)
-- **RAG (Retrieval-Augmented Generation)**: automatically queries ADB vector store for relevant context before generating responses β single ADB connection per search, smart table skip (only queries relevant tables based on question topic), CIS recommendation text filter for exact matching, follow-up context enrichment for short questions
+- **RAG (Retrieval-Augmented Generation)**: queries ADB vector store for relevant context before generating responses β single ADB connection per search, smart table skip, CIS recommendation text filter for exact matching, follow-up context enrichment
- **Source hierarchy**: findings tables > cisrecom (official remediation) > engineerknowledgebase (complementary) β temporal awareness with extract dates, tenancy isolation via JSON_VALUE filter
-- **MCP Tool Use (Function Calling)**: GenAI models can call tools from registered MCP servers during chat β supports both Cohere and Generic (OpenAI-style) function calling formats with automatic tool execution loop (max 5 iterations)
-- **Chat Memory Compaction**: automatic summarization of older messages when conversation exceeds ~6000 tokens β keeps 20 recent messages (10 interactions) intact and generates an LLM-based summary of older context
-- **Chat Sidebar**: collapsible right-side panel with model parameters (temperature, max_tokens, top_p, top_k, penalties) for quick adjustment during conversation
-- **Multimodal Chat**: upload images (PNG/JPG/GIF/WebP), PDFs, and text files directly in the chat for AI analysis β supports up to 5 files per message via OCI GenAI `ImageContent` and `DocumentContent`
-- **Async Background Processing**: chat requests return immediately, GenAI + MCP tools process in background via dedicated thread pool (10 threads), frontend polls for results β eliminates 504 timeouts on long-running scans
-- **Thinking Indicator**: button disables and shows spinner + "Pensando..." while waiting for GenAI response, with message timestamps
+- **MCP Tool Use (Function Calling)**: GenAI models call tools from registered MCP servers during chat β supports Cohere and Generic (OpenAI-style) function calling with automatic tool execution loop (max 5 iterations)
+- **Chat Memory Compaction**: automatic summarization of older messages when conversation exceeds ~6000 tokens β keeps 20 recent messages intact
+- **Chat Sidebar**: collapsible panel with model parameters (temperature, max_tokens, top_p, top_k, penalties)
+- **Multimodal Chat**: upload images (PNG/JPG/GIF/WebP), PDFs, and text files for AI analysis β up to 5 files per message via OCI GenAI `ImageContent` and `DocumentContent`
+- **Async Background Processing**: chat requests return immediately, GenAI + MCP tools process in background via dedicated thread pool (10 threads), frontend polls for results
- 16 chat models + 3 embedding models across 5 providers: **Meta** (Llama 4), **Google** (Gemini 2.5), **OpenAI** (GPT-5.2/5.1/5 Mini/4.1/4o, o3/o4-mini), **xAI** (Grok 4/3)
-- OCID-based model resolution: catalog maps model IDs to OCI resource IDs per region for reliable API calls
+- OCID-based model resolution: catalog maps model IDs to OCI resource IDs per region
- 16 OCI regions supported with auto-generated endpoints
-- **Provider parameter matrix**: automatic parameter segmentation per model β reasoning models (o3/o4-mini) only get `max_completion_tokens` + `reasoning_effort`, OpenAI GPT-4.x gets freq/pres penalties, Google/Meta get `top_k`, xAI gets basic params. Unsupported parameters are explicitly nulled to prevent SDK serialization errors
-- Full parameter control: temperature, max_tokens, top_p, top_k, frequency/presence penalty (when supported by model)
+- **Provider parameter matrix**: automatic parameter segmentation per model β reasoning models (o3/o4-mini) only get `max_completion_tokens` + `reasoning_effort`, OpenAI GPT-4.x gets freq/pres penalties, Google/Meta get `top_k`
+- Full parameter control: temperature, max_tokens, top_p, top_k, frequency/presence penalty (when supported)
- Toggle MCP tools on/off per chat session
- Conversation history with session management
- On-Demand and Dedicated serving modes
-### ποΈ Terraform Agent (IaC)
+### Terraform Agent (IaC)
- **AI-powered Terraform code generation** for OCI infrastructure provisioning
- Dedicated chat interface with OCI Terraform provider context
- **Workspace management**: create, plan, apply, destroy Terraform workspaces
-- **Compartment-aware**: browse and select target compartment for provisioning, with existing OCI resources injected as context
+- **Compartment-aware**: browse and select target compartment, with existing OCI resources injected as context
- **Plan/Apply/Destroy lifecycle**: full Terraform workflow with real-time output tracking
- **File management**: view generated `.tf` files, copy, download individually or as bundle
-- **Confirmation modal**: type "DESTROY" to confirm resource destruction
-- **Inline file editor**: click any generated `.tf` file to edit in-place with monospace editor, Tab support, and save β auto-syncs with workspace code
-- **Split-panel terminal**: real-time Plan/Apply/Destroy output in a dark terminal panel alongside files/plan/resources (always visible)
+- **Inline file editor**: click any `.tf` file to edit in-place with monospace editor, Tab support, and save
+- **Split-panel terminal**: real-time Plan/Apply/Destroy output in a dark terminal panel alongside files/plan/resources
- **Multi-file generation**: AI generates separate `.tf` files via `// filename:` markers (main.tf, variables.tf, outputs.tf, etc.)
-- **Smart file correction**: when fixing errors, model generates only changed files β frontend merges with existing workspace, preserving untouched files. Existing files, plan output, and apply errors are injected as context automatically
-- **14-point validation checklist**: system prompt enforces cross-references, CIDRs, security lists, route tables, DRG attachments, RPC peering, HCL block syntax, resource type validation, and variable declarations
-- **Auto-split pipeline (3 layers)**: frontend + backend auto-split of monolithic HCL files into categorized files (networking.tf, compute.tf, etc.) + backend deduplication to prevent duplicate resource errors
-- **HCL syntax fix**: automatic expansion of single-line blocks with multiple arguments into valid multi-line HCL format
-- **Resource type validation**: SQLite-based validation of ~937 OCI resource types with `difflib.get_close_matches` suggestions for invalid types β warnings appended automatically to model responses
+- **Smart file correction**: when fixing errors, model generates only changed files β frontend merges with existing workspace, preserving untouched files
+- **14-point validation checklist**: system prompt enforces cross-references, CIDRs, security lists, route tables, DRG attachments, RPC peering, HCL syntax, resource type validation, and variable declarations
+- **Auto-split pipeline (3 layers)**: frontend + backend auto-split of monolithic HCL files into categorized files + backend deduplication
+- **HCL syntax fix**: automatic expansion of single-line blocks into valid multi-line HCL format
+- **Resource type validation**: SQLite-based validation of ~937 OCI resource types with `difflib.get_close_matches` suggestions
- **Chat history**: ChatGPT-style session history with rename/delete, shared between Chat Agent and Terraform Agent
- **Resizable split view**: chat on top (50%), files/plan/resources + terminal on bottom (50%) with drag-to-resize
- Terraform CLI installed in container (v1.14.7)
-- **Terraform Resource Reference**: auto-generated OCI provider resource catalog (~937 resources) injected into system prompt β built from `terraform providers schema -json` at container startup, stored in SQLite, refreshable via menu button
-- **Official Resource Docs Injection**: on-demand fetch of Example Usage + Argument Reference from the official OCI Terraform provider docs (registry.terraform.io) β cached in SQLite, injected into model context based on detected resource types
-- **Smart variable resolution**: auto-generates `terraform.tfvars` by scanning declared variables and mapping them to OCI config values (`tenancy_ocid`, `user_ocid`, `fingerprint`, `compartment_id`, `ssh_public_key`) β region is never forced, respecting variable defaults
-- **Multi-region provider management**: auto-detects provider aliases and region variables in generated code, generates `provider.tf` with credentials and correct region references (`var.region` instead of hardcoded values)
-- **Region safety guard**: plan is blocked if the model does not declare `variable "region"` β prevents accidental provisioning in the wrong region by never falling back to hardcoded defaults
-- **Rollback button**: when `terraform apply` fails, a Rollback button appears allowing manual `terraform destroy` to clean up partially created resources (requires typing "ROLLBACK" to confirm)
-- **System prompt sync**: Terraform system prompt in code is automatically synced to DB on every restart β single source of truth
+- **Terraform Resource Reference**: auto-generated OCI provider resource catalog (~937 resources) injected into system prompt β built from `terraform providers schema -json`, stored in SQLite, refreshable via UI
+- **Official Resource Docs Injection**: on-demand fetch of Example Usage + Argument Reference from the official OCI Terraform provider docs β cached in SQLite
+- **Smart variable resolution**: auto-generates `terraform.tfvars` by scanning declared variables and mapping them to OCI config values
+- **Multi-region provider management**: auto-detects provider aliases and region variables, generates `provider.tf` with correct region references
+- **Region safety guard**: plan is blocked if the model does not declare `variable "region"` β prevents accidental provisioning in the wrong region
+- **Rollback button**: when `terraform apply` fails, a Rollback button appears for `terraform destroy` with confirmation
+- **Prompt Generator**: dedicated sub-menu for AI-powered prompt generation β curated models, same knowledge pipeline as Terraform Agent, official docs injection, copy & send to Terraform Agent
+- **Prompt Editor**: inline system prompt editor with auto-save (800ms debounce)
- Compact system prompt optimized for OCI Terraform provider best practices (100K max_tokens)
-- **Flat workspace enforcement**: system prompt prohibits `module` blocks (workspace has no subdirectories) and enforces single-file variable declarations (`variables.tf` only)
-### π Terraform Prompt Generator
-- **AI-powered prompt generation** for OCI infrastructure β generates structured, optimized prompts to feed into the Terraform Agent
-- Dedicated chat interface (sub-menu under Terraform) matching Terraform Agent layout
-- **Curated model selection**: only 7 recommended models (GPT-4.1, o3, o4-mini, GPT-5.1, GPT-5.2, Gemini 2.5 Pro, Gemini 2.5 Flash), grouped by provider
-- **Same knowledge pipeline as Terraform Agent**: compact resource reference + auto-detected resource types + official docs from GitHub (cached in SQLite)
-- **Official Terraform docs injection**: detects resource types from user message (keywords + explicit `oci_*` mentions), fetches Example Usage + Argument Reference from GitHub, cached in `tf_resource_docs` table
-- **Conversation history**: persistent sessions (agent_type `tf-prompt`) with sidebar history panel, rename/delete, session restore
-- **Contextual follow-up**: conversation history sent to GenAI for iterative prompt refinement
-- **Copy & Send**: action buttons on assistant messages to copy the prompt or send directly to the Terraform Agent input
-- Robust system prompt with OCI Provider constraints, inference rules, architecture templates, and workspace restrictions
-- Example prompts as clickable buttons for quick start
+### OCI Services
+- **Service Status tab**: auto-detect 6 security services (VSS, Data Safe, Cloud Guard, Bastion, Security Zones, Vault) per tenancy via OCI API with user overrides
+- **OCI Health tab**: real-time Oracle service health from ocistatus.oraclecloud.com β 49 regions, search, geographic filters
-### β‘ OCI Resource Actions
+### OCI Resource Actions
- **Start/Stop Compute Instances** directly from OCI Account Explorer with one click
- **Start/Stop Autonomous Databases** from Explorer with confirmation prompt
- **Start/Stop DB Systems, MySQL, Container Instances** β 5 resource types with operational control
@@ -99,8 +89,8 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
- Lifecycle state badges (RUNNING/STOPPED/AVAILABLE) with color indicators
- Actions audited in the audit log
-### π OCI Account Explorer
-- **KPI stats bar**: real-time resource counts per category with tooltip breakdown, cached in SQLite for instant loading
+### OCI Account Explorer
+- **KPI stats bar**: real-time resource counts per category with tooltip breakdown, cached in SQLite
- **Tree-view navigation** with resizable side panel (280px default) and drag handle
- **36 resource types** across 9 categories:
- **Compute**: Instances, Boot Volumes, Instance Pools
@@ -118,128 +108,124 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
- **Dead state filtering**: automatically hides TERMINATED/DELETED/DELETING resources
- **Background count refresh**: counts cached in SQLite, refreshed on compartment access without blocking UI
- **Security list detail**: view ingress/egress rules directly in the explorer
-- Select which OCI connection to explore
- Real-time API calls via OCI Python SDK
-### π CIS Compliance Reports (Oracle Official Engine)
+### CIS Compliance Reports (Oracle Official Engine)
- Powered by Oracle's official `cis_reports.py` (6660 lines, 48 CIS + 11 OBP checks)
- **Granular execution parameters**: CIS Level (1/2), OCI Best Practices, Raw Data, OCID Redaction
- - **Level 1**: Essential security controls that can be implemented with minimal impact on operations. Recommended as baseline for all organizations.
- - **Level 2**: Advanced security controls that may restrict functionality or require more effort to implement. Recommended for high-security environments.
- **Multiple output formats**: HTML summary, CSV per section/finding, JSON summary, optional XLSX
-- **Report history**: full execution history with status, tenancy filter, and download actions (HTML/JSON) in the Reports tab
-- **Delete reports**: remove completed reports and their associated files from the system
-- **File browser**: files grouped by section (Summary, CIS Findings, OBP, Raw Data, etc.) within the Reports page, with per-section embedding
-- **Embed individual files**: embed any report file (CSV, TXT, JSON, PDF) directly into the ADB vector store for RAG enrichment
+- **Report history**: full execution history with status, tenancy filter, and download actions
+- **Delete reports**: remove completed reports and their associated files
+- **File browser**: files grouped by section within the Reports page, with per-section embedding
+- **Embed individual files**: embed any report file (CSV, TXT, JSON, PDF) into the ADB vector store for RAG enrichment
- Region filtering with multi-select
- Real-time progress tracking with phase-based progress bar
-- **CIS Engine auto-update**: check for new versions of `cis_reports.py` from Oracle's GitHub repository and update with one click (admin only). Custom patches are automatically reapplied after update
+- **CIS Engine auto-update**: check for new versions from Oracle's GitHub and update with one click (admin only)
-### π LAD A-Team CIS Compliance Report
+### LAD A-Team CIS Compliance Report
- **Professional compliance report** following Oracle Cloud Security Assessment format (cover page, purpose statement, disclaimer, copyright)
- **Table of Contents** with links to each section and recommendation
- **Security Overview**: 7 Oracle security pillars (Customer Isolation, Data Encryption, Data Management, etc.)
+- **OCI Services section**: summary table of 6 security services per tenancy + detailed descriptions + Cloud Guard recommendations table
- **CIS Assessment Summary**: table with Domains / Total Controls / Failed / Passed per section
- **CIS Benchmark Recommendation Summary**: full table with Item / Description / Status (OK/FAILED)
- **Detailed findings**: per-recommendation cards with compliance percentage bar, result description, and remediation steps
-- **RAG-powered remediation**: enriched remediation steps fetched from ADB vector store (`CISRECOM` table) using cosine similarity search β filters by exact recommendation number, extracts only remediation section from matched chunks
-- **Affected resources CSV links**: non-compliant findings show a clickable link to download the CSV with affected resource details (authenticated via JWT token)
-- **Download PDF**: `window.print()` with CSS `@page A4` formatting for professional PDF output
-- **Caching**: generated HTML cached to disk, subsequent views are instant; "Regenerar" button clears cache and re-generates
+- **RAG-powered remediation**: enriched remediation steps from ADB vector store (`CISRECOM` table) using cosine similarity β filters by exact recommendation number
+- **Affected resources CSV links**: non-compliant findings show a clickable link to download the CSV with affected resource details (JWT auth)
+- **Back page**: Connect with us + copyright
+- **Download PDF**: `window.print()` with CSS `@page A4` formatting
+- **Download DOCX**: standalone DOCX export with Pillow camouflage strip, green header tables, result boxes, code blocks, A4 format
+- **compliance_data.json**: shared data source between HTML and DOCX (identical content)
+- **Progress bar during generation**: shows RAG progress (X/35), percentage, and current recommendation
+- **ADB connection pre-check**: fail fast with 503 if ADB is unreachable before starting generation
+- **Caching**: generated HTML cached to disk; "Regenerar" button clears cache and re-generates
- **Collapsible sections**: HTML report and compliance report start minimized, expandable on demand
-### π‘οΈ Built-in CIS MCP Server (Granular Per-Section)
+### Built-in CIS MCP Server (Granular Per-Section)
- **Auto-registered** CIS Compliance Scanner MCP server β available out of the box
- **12 granular tools** instead of monolithic full-tenancy scan:
- - `cis_scan_iam` β IAM checks (CIS 1.1β1.17): users, policies, groups, MFA, API keys
- - `cis_scan_networking` β Network checks (CIS 2.1β2.8): security lists, NSGs, VCNs
- - `cis_scan_compute` β Compute checks (CIS 3.1β3.3): instance metadata, monitoring
- - `cis_scan_logging_monitoring` β Logging/Monitoring checks (CIS 4.1β4.17): audit, alarms, events
- - `cis_scan_storage` β Storage checks (CIS 5.1β5.3): buckets, block volumes, file systems
- - `cis_scan_asset_management` β Asset checks (CIS 6.1β6.2): compartments, tagging
+ - `cis_scan_iam` β IAM checks (CIS 1.1-1.17): users, policies, groups, MFA, API keys
+ - `cis_scan_networking` β Network checks (CIS 2.1-2.8): security lists, NSGs, VCNs
+ - `cis_scan_compute` β Compute checks (CIS 3.1-3.3): instance metadata, monitoring
+ - `cis_scan_logging_monitoring` β Logging/Monitoring checks (CIS 4.1-4.17): audit, alarms, events
+ - `cis_scan_storage` β Storage checks (CIS 5.1-5.3): buckets, block volumes, file systems
+ - `cis_scan_asset_management` β Asset checks (CIS 6.1-6.2): compartments, tagging
- `cis_list_configs` / `cis_list_checks` β list available OCI configs and CIS checks
- `cis_get_check` / `cis_get_remediation` β detailed findings and remediation guidance
- `cis_get_scan_status` / `cis_invalidate_cache` β session status and cache management
-- **Per-section data collection**: each scan tool collects only the OCI data needed for that section, avoiding unnecessary API calls
-- **Region-specific scanning**: all scan tools accept optional `regions` parameter to target specific OCI regions (e.g., `["us-ashburn-1"]`) instead of scanning the entire tenancy
-- **Session caching**: collected data is cached per config+regions scope (2-hour TTL), so subsequent scans on different sections reuse shared prerequisites (compartments, identity domains)
-- **Compartment filtering**: scan results can be filtered by specific compartment, showing only relevant findings
-- **Collection error tracking**: errors during data collection are captured per section and reported to the caller
-- **Parallelized data collection**: base collectors and regional collectors run in parallel thread pools (up to 8 workers), with 30-minute timeout per tool call and 1 automatic retry on failure
-- Based on Oracle's official `cis_reports.py` (6660 lines, 48 CIS + 11 OBP checks)
+- **Per-section data collection**: each scan tool collects only the OCI data needed for that section
+- **Region-specific scanning**: all scan tools accept optional `regions` parameter to target specific OCI regions
+- **Session caching**: collected data cached per config+regions scope (2-hour TTL)
+- **Compartment filtering**: scan results can be filtered by specific compartment
+- **Parallelized data collection**: base and regional collectors run in parallel (up to 8 workers), 30-minute timeout with 1 automatic retry
-### π MCP Server Registry + Tool Discovery
+### MCP Server Registry + Tool Discovery
- Register multiple MCP servers (stdio, SSE, Python module)
- **Automatic tool discovery**: connect to MCP servers and discover available tools with names, descriptions, and input schemas
- **Manual tool definition**: add/edit tools with JSON Schema parameter definitions
-- **Chat Agent integration**: discovered tools are automatically available as GenAI function calls during chat
+- **Chat Agent integration**: discovered tools are automatically available as GenAI function calls
- Upload `.py` scripts directly to servers
- **Link MCP servers to ADB Vector** databases as tools
- Activate/deactivate servers
-- Select which MCP server to use per report execution
-### ποΈ Autonomous Database Vector Storage
+### Autonomous Database Vector Storage
- Oracle Autonomous Database connection with **mTLS Wallet** authentication
- `python-oracledb` Thin mode (no Oracle Client needed)
- Wallet ZIP upload and automatic extraction
- Connection testing with **case-insensitive table validation** against ADB `user_tables`
-- **Multiple vector tables per ADB**: register, edit, toggle active/inactive β tables managed inside the edit connection card
+- **Multiple vector tables per ADB**: register, edit, toggle active/inactive
- **Multi-table RAG search**: queries all active tables across all ADB configs, merges results by cosine distance
-- **Auto-resolve embedding config**: automatically uses existing OCI credentials for GenAI embeddings β no separate GenAI config required
-- **Case-insensitive table matching**: supports both uppercase and lowercase table names in Oracle ADB with quoted identifiers
+- **Auto-resolve embedding config**: automatically uses existing OCI credentials for GenAI embeddings
+- **Case-insensitive table matching**: supports both uppercase and lowercase table names with quoted identifiers
-### 𧬠Embeddings & Knowledge Base
-- **Auto-embed CIS Reports**: one-click embedding of all report CSVs β each CSV automatically mapped to its ADB table (identityandaccess, networking, computeinstances, etc.)
-- **Per-section embedding**: embed individual sections (Identity, Networking, Compute, etc.) with dedicated button per section
-- **CIS Recommendation tagging**: each embedded document includes CIS recommendation number, section, and status in the content for precise search
-- **Auto-detect embedding dimension**: detects table dimension (1536/3072) and selects correct model automatically
-- **Purge before re-embed**: automatically deletes old data for same tenancy/date before inserting new embeddings
+### Embeddings & Knowledge Base
+- **Auto-embed CIS Reports**: one-click embedding of all report CSVs β each CSV automatically mapped to its ADB table
+- **Per-section embedding**: embed individual sections with dedicated button per section
+- **CIS Recommendation tagging**: each embedded document includes CIS recommendation number, section, and status
+- **Auto-detect embedding dimension**: detects table dimension (1536/3072) from DDL (even on empty tables) and selects correct model automatically (1536 = small, 3072 = large)
+- **Purge before re-embed**: automatically deletes old data for same tenancy/date before inserting
- **Real-time progress**: progress bar with current section, global progress, queue of upcoming sections
- **Background persistence**: embedding continues when navigating away, progress resumes on return
- **Text chunking**: large CSV rows split into 8000-char chunks with context header (Tenancy, Resource, CIS Recommendation) repeated
-- **CIS Recommendations**: upload CIS PDF to populate the `cisrecom` vector table
+- **CIS PDF Chunker**: segments CIS PDF by recommendation number (X.X Ensure...), target 7000 chars with 500-char overlap, filters TOC/appendix/page headers β 54/54 recommendations with complete Description + Rationale + Remediation
+- **RAG timeout 60s + retry**: increased from 30s with no retry for improved reliability
+- **Direct SQL fetch**: retrieves chunks by metadata `recommendationNumber` for overlap resolution
- **Knowledge Base**: upload documents (`.txt`, `.pdf`, `.csv`, `.json`, `.md`) or **import URLs** to populate `engineerknowledgebase`
- **Consult Embeddings**: chat-like interface with tenancy selector β queries vector store with CIS number detection for exact matching, smart table skip, source citations
- **OCI GenAI Embeddings**: uses OpenAI Text Embedding 3 Large/Small via OCI GenAI
- 11 ADB vector tables supported (9 CIS report + 2 knowledge base)
-### π Configuration & Chat Logs
+### Configuration & Chat Logs
- **Persistent activity log** per configuration tab (OCI, GenAI, ADB, MCP)
-- **Chat audit logs**: all chat interactions (user messages, AI responses, tool calls, errors) logged with session/message ID, severity, and source
+- **Chat audit logs**: all chat interactions logged with session/message ID, severity, and source
- Logs all test, save, upload, report, and ingest operations with severity (success/error/info)
- **Inline log panel** at the bottom of each config tab with severity filter
- Auto-cleanup of logs older than 30 days
- Admin can view all logs; users see only their own
-- API endpoints for listing and clearing logs
-### π Export / Import Configuration
+### Export / Import Configuration
- **Full config export** as JSON: OCI credentials (with private keys), GenAI configs, MCP servers, system prompts, app settings, ADB vector configs
- **Import** from exported JSON with merge-by-ID (skips existing, prevents duplicates)
- Private keys exported as base64 and restored to disk on import
-- Enables full environment migration between machines (Mac β Windows β Linux)
+- Enables full environment migration between machines
-### π¨ Theme System & UI
+### Theme System & UI
- **Light/Dark mode toggle** in the sidebar footer
- **Oracle Dark Premium** design: deep dark backgrounds (#0D0F12), dark cards (#1A1D24), Oracle Red accent (#C74634)
- **Oracle-branded light mode**: warm light backgrounds (original theme)
- Theme preference persisted in localStorage across sessions
- All CSS driven by CSS custom properties β zero hardcoded colors
-- **SVG icon system**: 45+ Lucide-based inline SVG icons replace all emojis for cross-platform consistency (Windows/Mac/Linux)
-- **KPI Dashboard**: compliance score gauge (SVG semicircular arc with gradient), pass/fail/total KPI cards, donut chart (severity distribution), horizontal bar chart (findings by section) β powered by Recharts
-- **Animated UI**: staggered fade-in for cards/KPIs/tables, smooth theme transitions, hover effects with transforms, shimmer loading states, pulse alerts
+- **SVG icon system**: 45+ Lucide-based inline SVG icons for cross-platform consistency
+- **KPI Dashboard**: compliance score gauge (SVG semicircular arc with gradient), pass/fail/total KPI cards, donut chart, horizontal bar chart β powered by Recharts
+- **Animated UI**: staggered fade-in, smooth theme transitions, hover effects, shimmer loading states, pulse alerts
-### βοΈ Terraform Prompt Editor
-- **Inline prompt editor** accessible from the Terraform menu (3-dot menu β Editar Prompt)
-- Edit the active Terraform system prompt name and content
-- **Auto-save with debounce** (800ms): changes saved to database automatically as you type
-- Visual "Salvando..." β "Salvo β" indicator
-
-### π Security
+### Security
- **JWT authentication** with configurable expiry
- **TOTP MFA** (Google Authenticator / Authy compatible)
- **RBAC** with 3 roles: Admin, User, Viewer
- Audit logging for all operations
- Encrypted credential storage
+- **Per-user timezone** setting (not global)
---
@@ -266,6 +252,7 @@ The platform combines security compliance scanning, AI-powered chat with **RAG (
| | - GenAI --> LLM (16 models) | |
| | - oracledb --> ADB Vector | |
| | - Chromium --> PDF gen | |
+| | - Pillow --> DOCX gen | |
| | - Terraform CLI 1.14.7 | |
| | - SQLite (agent.db) | |
| | | |
@@ -325,7 +312,7 @@ Default credentials:
- **Username:** `admin`
- **Password:** `admin123`
-> β οΈ Change the default password immediately after first login.
+> Change the default password immediately after first login.
---
@@ -460,7 +447,7 @@ CREATE TABLE engineerknowledgebase (ID RAW(16), TEXT CLOB, METADATA JSON, EMBEDD
| Table Name | Purpose | How to populate |
|-----------|---------|-----------------|
-| `cisrecom` | CIS Benchmark official recommendations β remediation steps, rationale, audit procedures per control | Upload CIS PDF in Embeddings tab |
+| `cisrecom` | CIS Benchmark official recommendations β remediation steps, rationale, audit procedures per control | Upload CIS PDF in Embeddings tab (auto-chunked by recommendation) |
| `engineerknowledgebase` | General knowledge base β blogs, documentation, PDFs, URLs for complementary context | Upload files or import URLs in Embeddings tab |
> **How embedding works**: When you click **"Embed Report"** on a completed CIS report, the system automatically:
@@ -506,19 +493,19 @@ Allow group to read buckets in compartment
```
oci-cis-agent/
βββ backend/
-β βββ app.py # FastAPI application (~7700 lines)
+β βββ app.py # FastAPI application (~9800 lines)
β βββ cis_reports.py # Oracle CIS Benchmark checker (6660 lines, report engine)
β βββ mcp_cis_server.py # MCP server with 12 granular CIS tools (~700 lines)
β βββ gen_tf_reference.py # OCI Terraform provider resource catalog generator
β βββ Dockerfile # Python 3.12 + OCI CLI + Terraform 1.14.7 + Chromium
β βββ requirements.txt # Dependencies
βββ frontend-react/
-β βββ src/ # React 19 SPA (TypeScript, 38 source files, ~15500 lines)
-β β βββ pages/ # 15 page components (Chat, Terraform, Explorer, Reports, Config, Admin)
+β βββ src/ # React 19 SPA (TypeScript, 39 source files, ~15800 lines)
+β β βββ pages/ # 16 page components (Chat, Terraform, Explorer, OCI Services, Reports, Config, Admin)
β β βββ api/ # API client + endpoint modules
β β βββ stores/ # Zustand stores (app state persistence across navigation)
β β βββ hooks/ # Custom hooks (theme, polling)
-β β βββ i18n/ # Internationalization (pt/en, 700+ keys)
+β β βββ i18n/ # Internationalization (pt/en, 703 keys)
β βββ dist/ # Built SPA served at /
βββ nginx/
β βββ default.conf # Reverse proxy (React SPA + API /api/)
@@ -598,6 +585,14 @@ oci-cis-agent/
| GET | `/api/oci/explore/{id}/counts` | Get cached resource counts (instant from SQLite) |
| POST | `/api/oci/explore/{id}/counts/refresh` | Refresh all resource counts in background |
+### OCI Services
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | `/api/oci-services/{config_id}` | Get service status per tenancy |
+| PUT | `/api/oci-services/{config_id}` | Update service overrides |
+| GET | `/api/oci-health` | Oracle Cloud status proxy (49 regions) |
+
### OCI Resource Actions
| Method | Endpoint | Description |
@@ -704,7 +699,16 @@ oci-cis-agent/
| GET | `/api/reports/{rid}/files/{fid}/download` | Download individual report file |
| GET | `/api/reports/{rid}/compliance-report` | Serve cached LAD A-Team compliance report HTML |
| POST | `/api/reports/{rid}/compliance-report/generate` | Generate compliance report (background, with RAG) |
-| GET | `/api/reports/{rid}/compliance-report/status` | Check if compliance report is ready |
+| GET | `/api/reports/{rid}/compliance-report/status` | Check compliance report status (returns progress: current, total, step, rec) |
+| GET | `/api/reports/{rid}/compliance-report/docx` | Download compliance report as DOCX |
+
+### User Settings
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| GET | `/api/users/me/timezone` | Get user timezone |
+| PUT | `/api/users/me/timezone` | Set user timezone |
+| GET | `/api/settings/timezone/options` | List available timezone options |
### CIS Engine
@@ -785,6 +789,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
| OCI SDK | `oci` 2.133.0, `oci-cli` |
| ADB | `python-oracledb` 2.4.1 (Thin mode) |
| GenAI | `oci.generative_ai_inference` |
+| Image Processing | Pillow (DOCX report generation) |
| Container | Docker Compose, Nginx reverse proxy |
| MCP | Model Context Protocol SDK (stdio/SSE) with tool discovery + execution |
| Terraform | Terraform CLI 1.14.7, OCI Provider |
@@ -796,9 +801,10 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
| Version | Date | Changes |
|---------|------|---------|
+| **v3.0** | 2026-03 | **OCI Services page**: new menu item with Service Status tab (auto-detect 6 security services β VSS, Data Safe, Cloud Guard, Bastion, Security Zones, Vault β per tenancy via OCI API + user overrides) and OCI Health tab (real-time Oracle service health from ocistatus.oraclecloud.com, 49 regions, search, geo filters). **Compliance Report v3.0**: OCI Services section with summary table + detailed descriptions + Cloud Guard recommendations, back page (Connect with us + copyright), DOCX download (Pillow camouflage strip, green header tables, result boxes, code blocks, A4 format), compliance_data.json shared between HTML and DOCX, progress bar during RAG generation (X/35, percentage), ADB connection pre-check (fail fast 503). **CIS PDF Chunker**: segments CIS PDF by recommendation number (X.X Ensure...), target 7000 chars with 500-char overlap, filters TOC/appendix/page headers β 54/54 recommendations with complete Description + Rationale + Remediation. **Embedding improvements**: auto-detect dimension from DDL (even on empty tables), auto-detect model per dimension (1536=small, 3072=large), RAG timeout 60s + retry (was 30s), direct SQL fetch by metadata recommendationNumber for overlap chunks. **Per-user timezone** (not global). **New API endpoints**: OCI Health proxy, OCI Services status/overrides, user timezone CRUD, DOCX download, compliance progress details, timezone options. |
| **v2.8** | 2026-03 | **Legacy frontend removed**: React SPA now served at root `/` (no more `/app/` prefix), legacy vanilla JS SPA removed. **Compliance Report ZIP download**: Chromium headless PDF generation (identical to browser print) + CSV findings bundled in ZIP. **Compliance generation state persisted server-side**: `.compliance_generating` marker file ensures generation status survives page navigation β spinner auto-resumes on return. **RAG remediation enriched**: extracts Description, Rationale, Audit, and Remediation sections from CIS vector chunks, combines up to 3 matched chunks. **Chat markdown styling**: full markdown rendering with syntax highlighting (Catppuccin Mocha theme via rehype-highlight), styled headings, lists, tables, blockquotes, inline/block code. **Default model auto-select**: Chat Agent auto-selects first GenAI config on page load. **Explorer silent polling**: no more flickering during start/stop resource actions. |
| **v2.7** | 2026-03 | **LAD A-Team CIS Compliance Report**: professional Oracle-format compliance report with cover page, TOC, Security Overview (7 pillars), CIS Assessment Summary, detailed findings with compliance % bars, RAG-powered remediation from ADB vector store (`CISRECOM` table with strict recommendation number filtering), affected resources CSV download links per non-compliant finding (JWT auth via query param). **React SPA**: 15-page React 19 frontend (TypeScript, Vite, Zustand, 38 source files). **i18n**: full internationalization with 625 keys (pt/en), language switcher persisted in localStorage. **Report management**: delete reports endpoint, embed individual report files (CSV/TXT/JSON/PDF) into vector store. **Terraform ZIP download**: replaced individual file downloads with client-side ZIP generation (pure JS, no external lib). **Explorer UX**: silent polling during start/stop actions eliminates flickering (resources update in-place without clearing the list). **Report sections collapsed by default**: HTML report and compliance report iframes start minimized. |
-| **v2.6** | 2026-03 | **Rename**: app renamed to "AI Agent β Infrastructure & Security Engineer". **Session Token Auth**: OCI session token authentication support (type selector API Key/Session Token, conditional form fields, collapsible credential form, type tags in credentials table, `SecurityTokenSigner` backend). **Terraform Workspaces submenu**: sidebar sub-tab listing all workspaces grouped by date with accordion UI, filtered by existing chat sessions (INNER JOIN), actions (plan/apply/destroy/delete/open). **Retry on failure**: "Reenviar" button on failed messages across Chat Agent, Terraform Agent, and Prompt Generator. **Terraform fixes**: `f2` undefined variable in dedup logic, `oci_core_services` false positive validation (data sources excluded), `S.tfCode` reconstruction from `S.tfFiles` for Re-plan, auto-fix button visible without pre-selected model, DRG attachment rules in system prompt. **Resilience**: stuck workspace recovery on container restart (planning/applying/destroying β reset + lock file cleanup). |
+| **v2.6** | 2026-03 | **Rename**: app renamed to "AI Agent β Infrastructure & Security Engineer". **Session Token Auth**: OCI session token authentication support (type selector API Key/Session Token, conditional form fields, collapsible credential form, type tags in credentials table, `SecurityTokenSigner` backend). **Terraform Workspaces submenu**: sidebar sub-tab listing all workspaces grouped by date with accordion UI, filtered by existing chat sessions (INNER JOIN), actions (plan/apply/destroy/delete/open). **Retry on failure**: "Reenviar" button on failed messages across Chat Agent, Terraform Agent, and Prompt Generator. **Terraform fixes**: `f2` undefined variable in dedup logic, `oci_core_services` false positive validation (data sources excluded), `S.tfCode` reconstruction from `S.tfFiles` for Re-plan, auto-fix button visible without pre-selected model, DRG attachment rules in system prompt. **Resilience**: stuck workspace recovery on container restart (planning/applying/destroying reset + lock file cleanup). |
| **v2.5** | 2026-03 | **Explorer Expansion**: KPI stats bar with per-category resource counts (cached in SQLite `explorer_counts_cache`, background refresh on compartment access), enhanced views for VCNs/Subnets/Load Balancers/Buckets with expandable cards, start/stop for DB Systems + MySQL + Container Instances (5 resource types total), dead state filtering (`TERMINATED`/`DELETED`/`DELETING` hidden across all 36 endpoints), wider compartment tree (280px), IAM excluded from compartment totals. **Prompt Generator Intelligence**: same knowledge pipeline as Terraform Agent β auto-detects resource types from user message, fetches official docs (Example Usage + Arguments) from GitHub cached in SQLite, uses compact categorized resource reference. **Network Firewall fix**: `NetworkFirewallPolicySummaryCollection` not iterable β added `.items` accessor for collection objects. |
| **v2.4** | 2026-03 | **Terraform Safety & UX**: region safety guard (plan blocked if model omits `variable "region"` β prevents wrong-region provisioning), rollback button on apply failure (manual `terraform destroy` with "ROLLBACK" confirmation), SSH public key field in OCI configs (auto-injected into `terraform.tfvars` for compute instances), reasoning_effort uppercase fix for OCI SDK, split-panel layout (terminal right 40%, files/plan/resources below chat 60%), plan button persists after destroy. **Terraform tfvars**: expanded `oci_var_map` with `ssh_public_key`/`ssh_authorized_keys` auto-mapping from OCI config |
| **v2.3** | 2026-03 | **Consult Embeddings**: dedicated sub-menu with chat-like interface for natural language Q&A against vector store β queries all active tables via cosine similarity, returns contextual answers with source citations (works with or without GenAI config). **Knowledge Base & RAG Enhancements**: Knowledge Base (Base de Conhecimento) with file upload + URL import to `engineerknowledgebase` vector table, CIS Recommendations upload to `cisrecom` table, URL content extraction (HTML tag/script stripping, remote PDF parsing), auto-resolve embedding config from OCI credentials (no separate GenAI config required), `report_date` metadata tracking for embedding freshness, purge & re-embed flow for updated reports. **ADB Vector Improvements**: case-insensitive table matching with quoted identifiers for Oracle ADB, table validation endpoint against `user_tables`, tables section moved inside edit connection card, case-preserving table registration (removed forced uppercase). **Vector search fix**: FLOAT32 compatibility for VECTOR_DISTANCE queries, base64-decoded compartment_id for auto-resolved OCI configs. **Embeddings tab cleanup**: removed delete buttons, simplified to read-only view, updated descriptions. **Dependencies**: added PyPDF2 for PDF text extraction |
@@ -813,7 +819,7 @@ All models include OCID mapping for `us-ashburn-1`. For other regions, use the "
| **v1.4** | 2026-03 | In-place config editing, 69 chat + 11 embedding models with OCID resolution (OpenAI GPT-5.3/5.2/5.1/5/4.1/4o/Codex/Image/Audio, xAI, Google, Meta, ProtectAI), OpenAI Text Embedding 3, custom OCID support, wallet auto-parse with DSN extraction |
| **v1.3** | 2026-03 | Persistent config logs per tab, GenAI auto-fill from OCI credentials, inline UX feedback with loading spinners, MCP type-switch fix, encrypted key passphrase support, Docker stdin hang fix |
| **v1.2** | 2026-03 | RAG pipeline (OCI GenAI embeddings + ADB vector search), dedicated Embeddings tab, CIS report chunking, file upload embedding |
-| **v1.1** | 2025-02 | OCI SDK GenAI (exact pattern), OCI Account Explorer, MCPβADB linking, full chat parameters |
+| **v1.1** | 2025-02 | OCI SDK GenAI (exact pattern), OCI Account Explorer, MCP-ADB linking, full chat parameters |
| **v1.0** | 2025-02 | Initial release: OCI theme, GenAI integration, MCP servers, ADB vector, JWT+MFA+RBAC |
---
@@ -872,5 +878,5 @@ MIT
---
- Built with β€οΈ for Oracle Cloud Infrastructure security compliance
-
\ No newline at end of file
+ Built for Oracle Cloud Infrastructure security compliance
+
diff --git a/backend/app.py b/backend/app.py
index d14295e..53d039d 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -17,7 +17,7 @@ from fastapi import (
Query, Body, BackgroundTasks
)
from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import JSONResponse, FileResponse, StreamingResponse, HTMLResponse
+from fastapi.responses import JSONResponse, FileResponse, StreamingResponse, HTMLResponse, Response
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
import jwt as pyjwt
@@ -2914,14 +2914,29 @@ def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str, max_chars:
_DIM_TO_MODEL = {1536: "openai.text-embedding-3-small", 3072: "openai.text-embedding-3-large"}
def _get_table_embedding_dim(cfg: dict, table_name: str) -> int:
- """Detect the embedding dimension of a table by sampling one row."""
+ """Detect the embedding dimension of a table by sampling one row, or from column definition if empty."""
conn = _get_adb_connection(cfg)
try:
cur = conn.cursor()
+ # Try from data first
cur.execute(f'SELECT VECTOR_DIMENSION_COUNT(EMBEDDING) FROM "{table_name}" FETCH FIRST 1 ROWS ONLY')
row = cur.fetchone()
- cur.close()
- return int(row[0]) if row and row[0] else 0
+ if row and row[0]:
+ cur.close()
+ return int(row[0])
+ # Table is empty β check column definition from DDL
+ try:
+ cur.execute(f"""SELECT DBMS_METADATA.GET_DDL('TABLE', :1) FROM DUAL""", [table_name])
+ ddl_row = cur.fetchone()
+ cur.close()
+ if ddl_row and ddl_row[0]:
+ import re
+ m = re.search(r'VECTOR\((\d+)', str(ddl_row[0]))
+ if m:
+ return int(m.group(1))
+ except Exception:
+ pass
+ return 0
except Exception as e:
log.warning(f"Failed to get embedding dimension for table '{table_name}': {e}")
return 0
@@ -3677,6 +3692,16 @@ def _ingest_documents_task(cfg: dict, genai_cfg: dict, documents: list, user_id:
import array
emb_model = cfg.get("embedding_model_id", "cohere.embed-v4.0")
table_name = table_name or cfg.get("table_name", "")
+ # Auto-detect embedding dimension from existing table data and use matching model
+ try:
+ actual_dim = _get_table_embedding_dim(cfg, table_name)
+ if actual_dim and actual_dim in _DIM_TO_MODEL:
+ detected_model = _DIM_TO_MODEL[actual_dim]
+ if detected_model != emb_model:
+ log.info(f"Ingest: table '{table_name}' has {actual_dim} dims, switching model from {emb_model} to {detected_model}")
+ emb_model = detected_model
+ except Exception as e:
+ log.warning(f"Ingest: failed to detect dimension for '{table_name}': {e}")
total = len(documents)
# Track status
if task_id:
@@ -3776,17 +3801,198 @@ def _chunk_report_by_section(report_data: dict) -> list:
})
return documents
-def _chunk_text_file(text: str, filename: str, chunk_size: int = 1000) -> list:
- """Split text into chunks by paragraphs or fixed size."""
+def _chunk_cis_pdf(text: str, filename: str, target_chars: int = 7000, overlap_chars: int = 500) -> list:
+ """Chunk a CIS Foundations Benchmark PDF by recommendation number.
+ Each recommendation (1.1, 1.2, etc.) becomes one or more chunks with overlap.
+ Port of the JavaScript embedding pipeline."""
+ import re as _re
+
+ def normalize(t):
+ t = t.replace('\r', '\n')
+ t = _re.sub(r'[ \t]+\n', '\n', t)
+ t = _re.sub(r'\n{3,}', '\n\n', t)
+ return t.strip()
+
+ def strip_page_headers(t):
+ # Remove "Page XX" both standalone and at start of lines
+ t = _re.sub(r'^\s*Page\s+\d+\s*$', '', t, flags=_re.MULTILINE | _re.IGNORECASE)
+ t = _re.sub(r'^Page\s+\d+\s+', '', t, flags=_re.MULTILINE | _re.IGNORECASE)
+ return t
+
+ def remove_toc(t):
+ # Remove everything from "Table of Contents" up to the actual recommendations section
+ # The real content starts with "Recommendations\n1 Identity" or "Profile Applicability"
+ toc_start = _re.search(r'\bTable of Contents\b', t, _re.IGNORECASE)
+ if not toc_start:
+ return t
+ # Find where actual recommendation content begins
+ content_start = _re.search(r'\bRecommendations\s*\n\s*1\s+Identity', t[toc_start.start():], _re.IGNORECASE)
+ if not content_start:
+ content_start = _re.search(r'\bProfile Applicability\b', t[toc_start.start():], _re.IGNORECASE)
+ if not content_start:
+ content_start = _re.search(r'\bOverview\b', t[toc_start.start():], _re.IGNORECASE)
+ if not content_start:
+ return t
+ end_pos = toc_start.start() + content_start.start()
+ if end_pos <= toc_start.start():
+ return t
+ return normalize(t[:toc_start.start()] + '\n\n' + t[end_pos:])
+
+ def is_chapter_header(line):
+ l = line.strip()
+ return bool(_re.match(r'^\d+\s+[A-Za-z].+', l)) and not _re.match(r'^\d+\.\d+', l)
+
+ def is_rec_header_start(line):
+ l = line.strip()
+ # Must be "1.1 Word..." but NOT a TOC line (with dots/page numbers)
+ if not _re.match(r'^\d+\.\d+(\.\d+)?\s+[A-Z]', l):
+ return False
+ # Skip TOC lines: contain "...." or end with a page number
+ if '....' in l or _re.search(r'\.\s*\d+\s*$', l):
+ return False
+ return True
+
+ def header_looks_complete(h):
+ # Complete if has (Manual)/(Automated) or ends with a closing paren
+ if _re.search(r'\(\s*(Manual|Automated)\s*\)', h, _re.IGNORECASE):
+ return True
+ # Also stop if next line starts a known section like "Profile Applicability"
+ return False
+
+ def chunk_text(t):
+ if not t:
+ return []
+ paragraphs = [p.strip() for p in t.split('\n\n') if p.strip()]
+ chunks = []
+ buf = ""
+ def push():
+ nonlocal buf
+ b = buf.strip()
+ if b:
+ chunks.append(b)
+ buf = ""
+ for p in paragraphs:
+ if len(p) > target_chars:
+ push()
+ i = 0
+ while i < len(p):
+ chunks.append(p[i:i + target_chars].strip())
+ i += max(1, target_chars - overlap_chars)
+ continue
+ candidate = f"{buf}\n\n{p}" if buf else p
+ if len(candidate) <= target_chars:
+ buf = candidate
+ else:
+ push()
+ if chunks and overlap_chars > 0:
+ prev = chunks[-1]
+ overlap = prev[max(0, len(prev) - overlap_chars):]
+ buf = f"{overlap}\n\n{p}".strip()
+ else:
+ buf = p
+ push()
+ return chunks
+
+ def remove_appendix(t):
+ """Remove appendix sections (Assessment Status, Change History, etc.) that pollute embeddings."""
+ for marker in [r'\bAppendix\b', r'\bAssessment Status\b', r'\bChange History\b',
+ r'\bCIS Controls v\d', r'\bDate\s+Version\s+Changes']:
+ m = _re.search(marker, t, _re.IGNORECASE)
+ if m and m.start() > len(t) * 0.7: # only cut if in last 30% of doc
+ t = t[:m.start()].rstrip()
+ break
+ return t
+
+ # Pipeline
+ text = normalize(text)
+ text = strip_page_headers(text)
+ text = remove_toc(text)
+ text = remove_appendix(text)
+ lines = text.split('\n')
+
+ # Segment by recommendation
+ segments = []
+ current = None
+ current_chapter = ""
+
+ i = 0
+ while i < len(lines):
+ line = lines[i]
+ if is_chapter_header(line):
+ current_chapter = line.strip()
+ if is_rec_header_start(line):
+ if current:
+ segments.append(current)
+ header = line.strip()
+ j = i + 1
+ while j < len(lines) and not header_looks_complete(header):
+ next_line = lines[j].strip()
+ if is_rec_header_start(next_line):
+ break
+ # Stop consuming if we hit a known section start
+ if next_line.startswith('Profile Applicability') or next_line.startswith('Description:'):
+ break
+ if next_line:
+ header = _re.sub(r'\s+', ' ', f"{header} {next_line}").strip()
+ j += 1
+ i = j - 1
+ current = {"header": header, "chapter": current_chapter, "body_lines": []}
+ i += 1
+ continue
+ if current:
+ current["body_lines"].append(line)
+ i += 1
+
+ if current:
+ segments.append(current)
+
+ # Generate chunks
+ documents = []
+ for seg in segments:
+ body = normalize('\n'.join(seg["body_lines"]))
+ if not body:
+ continue
+ rec_match = _re.match(r'^(\d+(\.\d+)+)', seg["header"])
+ rec_number = rec_match.group(1) if rec_match else "unknown"
+ canonical = normalize('\n'.join(filter(None, [
+ f"Recommendation: {seg['header']}",
+ f"Chapter: {seg['chapter']}" if seg['chapter'] else "",
+ "",
+ body,
+ ])))
+ chunks = chunk_text(canonical)
+ for idx, chunk in enumerate(chunks):
+ documents.append({
+ "content": chunk,
+ "source": filename,
+ "metadata": json.dumps({
+ "filename": filename,
+ "recommendationNumber": rec_number,
+ "chapter": seg["chapter"],
+ "source": "CIS-OCI-PDF",
+ "chunkIndex": idx + 1,
+ "chunkCount": len(chunks),
+ }),
+ })
+
+ log.info(f"CIS PDF chunking: {len(segments)} recommendations β {len(documents)} chunks from {filename}")
+ return documents
+
+
+def _chunk_text_file(text: str, filename: str, chunk_size: int = 1000, overlap: int = 200) -> list:
+ """Split text into chunks by paragraphs with overlap to avoid losing context at boundaries."""
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
documents = []
current_chunk = ""
+ prev_tail = "" # last N chars of previous chunk for overlap
chunk_num = 1
for para in paragraphs:
if len(current_chunk) + len(para) + 2 > chunk_size and current_chunk:
documents.append({"content": current_chunk, "source": filename, "metadata": f"chunk: {chunk_num}"})
chunk_num += 1
- current_chunk = para
+ # Keep overlap from end of current chunk
+ prev_tail = current_chunk[-overlap:] if len(current_chunk) > overlap else current_chunk
+ current_chunk = prev_tail + "\n\n" + para
else:
current_chunk = current_chunk + "\n\n" + para if current_chunk else para
if current_chunk:
@@ -4334,9 +4540,9 @@ async def embed_report_file(rid: str, fid: str, req: dict, bg: BackgroundTasks,
def _extract_pdf_text(file_bytes: bytes) -> str:
"""Extract text from a PDF file using PyPDF2 or pdfplumber."""
+ import io
try:
import PyPDF2
- import io
reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
pages = []
for page in reader.pages:
@@ -4348,7 +4554,6 @@ def _extract_pdf_text(file_bytes: bytes) -> str:
pass
try:
import pdfplumber
- import io
pages = []
with pdfplumber.open(io.BytesIO(file_bytes)) as pdf:
for page in pdf.pages:
@@ -4373,10 +4578,16 @@ async def embed_upload(adb_config_id: str = Form(...), table_name: str = Form(""
else:
content = raw.decode("utf-8", errors="replace")
if not content.strip(): raise HTTPException(400, "File is empty")
- documents = _chunk_text_file(content, file.filename)
+ target_table = table_name.strip() or None
+ # Use CIS-specific chunking for cisrecom table (segments by recommendation number with overlap)
+ if target_table and 'cisrecom' in target_table.lower():
+ documents = _chunk_cis_pdf(content, file.filename)
+ if not documents:
+ documents = _chunk_text_file(content, file.filename) # fallback
+ else:
+ documents = _chunk_text_file(content, file.filename)
if not documents: raise HTTPException(400, "No content chunks found")
cfg, gc = _get_adb_and_genai(adb_config_id)
- target_table = table_name.strip() or None
bg.add_task(_ingest_documents_task, cfg, gc, documents, u["id"], u["username"], table_name=target_table)
_audit(u["id"], u["username"], "embed_upload", file.filename, f"{len(documents)} chunks")
return {"ok": True, "message": f"Embedding de {len(documents)} chunks iniciado", "chunks": len(documents), "filename": file.filename}
@@ -5024,8 +5235,8 @@ def _format_remediation_html(text: str) -> str:
code_buf.append(stripped)
else:
flush_code()
- # "From Console:", "From CLI:", "From Command Line:" β bold header
- header_match = _re.match(r'^(From\s+(?:Console|CLI|Command Line|the Console|the CLI|OCI Console|OCI CLI)[:\s]*)(.*)', stripped, _re.IGNORECASE)
+ # "From Console:", "From CLI:", "From Command Line:", "Audit / Verification:" β bold header
+ header_match = _re.match(r'^(From\s+(?:Console|CLI|Command Line|the Console|the CLI|OCI Console|OCI CLI)[:\s]*|Audit\s*/?\s*Verification[:\s]*)(.*)', stripped, _re.IGNORECASE)
if header_match:
rest = esc(header_match.group(2)) if header_match.group(2) else ''
html_parts.append(f'{esc(header_match.group(1).strip())} {rest}
')
@@ -5094,22 +5305,53 @@ def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict:
if all_docs:
import re as _re
all_docs.sort(key=lambda d: d.get("distance", 999))
- # STRICT FILTER: only keep chunks that mention THIS exact recommendation number
+ # FILTER: keep chunks that mention THIS recommendation number in text OR metadata
exact_pattern = _re.compile(
r'(?:Recommendation[:\s]*' + _re.escape(rec_num) + r'|^' + _re.escape(rec_num) + r')\s',
_re.MULTILINE | _re.IGNORECASE
)
- matched = [d["content"] for d in all_docs
- if d.get("content") and len(d["content"]) > 500
- and exact_pattern.search(d["content"])]
+ matched = []
+ for d in all_docs:
+ content = d.get("content", "")
+ if not content or len(content) < 200:
+ continue
+ # Match by text content
+ if exact_pattern.search(content):
+ matched.append(content)
+ continue
+ # Match by metadata recommendationNumber (for overlap chunks)
+ meta = d.get("metadata", "")
+ if isinstance(meta, str) and f'"recommendationNumber": "{rec_num}"' in meta:
+ matched.append(content)
+ continue
+ if isinstance(meta, str) and f'"recommendationNumber":"{rec_num}"' in meta:
+ matched.append(content)
+ # Also fetch ALL chunks for this rec from ADB directly (catches overlap chunks)
+ try:
+ conn2 = _get_adb_connection(adb_cfg)
+ cur2 = conn2.cursor()
+ for tbl_name in search_tables:
+ cur2.execute(f'''SELECT "TEXT" FROM "{tbl_name}"
+ WHERE JSON_VALUE("METADATA", '$.recommendationNumber') = :1
+ ORDER BY JSON_VALUE("METADATA", '$.chunkIndex')''', [rec_num])
+ for row in cur2:
+ txt = row[0].read() if hasattr(row[0], 'read') else row[0]
+ if txt and txt not in matched:
+ matched.append(txt)
+ cur2.close()
+ conn2.close()
+ except Exception as e:
+ log.warning(f"Direct chunk fetch for {rec_num}: {e}")
+
if matched:
- best = matched[0]
- desc = _extract_description_section(best)
- rationale = _extract_rationale_section(best)
- rem = _extract_remediation_section(best)
- audit = _extract_audit_section(best)
- # Try additional chunks for fuller content
- for extra in matched[1:3]:
+ # Concatenate all chunks for the best possible extraction
+ full_text = '\n\n'.join(matched)
+ desc = _extract_description_section(full_text)
+ rationale = _extract_rationale_section(full_text)
+ rem = _extract_remediation_section(full_text)
+ audit = _extract_audit_section(full_text)
+ # Fallback: try individual chunks if full concatenation missed something
+ for extra in matched[1:4]:
if not rem:
rem = _extract_remediation_section(extra)
elif len(rem) < 200:
@@ -5120,8 +5362,25 @@ def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict:
audit = _extract_audit_section(extra)
if not desc:
desc = _extract_description_section(extra)
+ elif desc and not desc.rstrip().endswith(('.', ':', '!', '?', ';')):
+ # Description looks truncated β append continuation from next chunk
+ extra_desc = _extract_description_section(extra)
+ if extra_desc:
+ desc = desc.rstrip() + "\n" + extra_desc
if not rationale:
rationale = _extract_rationale_section(extra)
+
+ # Also check non-matched docs for continuation (same CIS but different chunk)
+ all_contents = [d["content"] for d in all_docs if d.get("content") and len(d["content"]) > 200]
+ for extra_content in all_contents[:6]:
+ if extra_content in matched:
+ continue
+ if not rem:
+ rem = _extract_remediation_section(extra_content)
+ if desc and not desc.rstrip().endswith(('.', ':', '!', '?', ';')):
+ extra_desc = _extract_description_section(extra_content)
+ if extra_desc and extra_desc not in desc:
+ desc = desc.rstrip() + "\n" + extra_desc
# Combine remediation + audit for complete guidance
full_rem = rem or ""
if audit and len(audit) > 50:
@@ -5245,7 +5504,6 @@ def _check_oci_services(config_id: str) -> list[dict]:
# DB stores base64 β decode if needed
compartment_raw = (cfg["compartment_id"] or "") if cfg else ""
if compartment_raw and not compartment_raw.startswith("ocid"):
- import base64
try:
compartment_raw = base64.b64decode(compartment_raw).decode("utf-8")
except Exception:
@@ -5464,6 +5722,35 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
def esc(v): return (v or "").replace("&","&").replace("<","<").replace(">",">").replace('"',""")
def slug(s): return _re.sub(r"[^\w-]","", _re.sub(r"\s+","-",(s or "").strip().lower()))
+ def _join_broken_lines(text):
+ """Join single-newline broken lines into flowing paragraphs.
+ Preserves: double-newline paragraph breaks, numbered lists, bullet lists, headers."""
+ if not text:
+ return text
+ lines = text.split('\n')
+ result = []
+ for line in lines:
+ line = line.strip()
+ if not line:
+ result.append('\n')
+ continue
+ # Never join if this line starts a list item or header
+ starts_new = (
+ _re.match(r'^\d+[\.\)]\s', line) or # "1. ", "2) "
+ _re.match(r'^[a-z][\.\)]\s', line) or # "a. ", "b) "
+ line.startswith('- ') or line.startswith('* ') or # bullets
+ line.startswith('From ') or # "From Console:", "From CLI:"
+ line.startswith('Audit') or # "Audit / Verification:"
+ line.startswith('Note:') or line.startswith('See ')
+ )
+ if starts_new:
+ result.append(line)
+ elif result and result[-1] != '\n' and not result[-1].endswith(('.', ':', '!', '?', ';')):
+ result[-1] = result[-1] + ' ' + line
+ else:
+ result.append(line)
+ return '\n'.join(result).strip()
+
total_all = len(filtered_rows)
total_passed = sum(1 for r in filtered_rows if r["__compliant"])
total_failed = total_all - total_passed
@@ -5529,22 +5816,52 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
detail = f" ({findings_num} of {esc(total_items)} items)"
result_html = f'Result: Tenancy {esc(tenancy_name)}: Non-Compliant{detail}
'
- # Description β split into paragraphs on double newlines, justify each
+ def _text_to_html_paras(text):
+ """Convert text to HTML paragraphs, rendering numbered lists as and bullets as ."""
+ cleaned = _join_broken_lines(text)
+ parts = []
+ lines = cleaned.split('\n')
+ ol_items = []
+ ul_items = []
+ def flush_ol():
+ if ol_items:
+ parts.append('' + ''.join(f'- {esc(i)}
' for i in ol_items) + '
')
+ ol_items.clear()
+ def flush_ul():
+ if ul_items:
+ parts.append('' + ''.join(f'- {esc(i)}
' for i in ul_items) + '
')
+ ul_items.clear()
+ for line in lines:
+ line = line.strip()
+ if not line:
+ flush_ol()
+ flush_ul()
+ continue
+ num_match = _re.match(r'^(\d+)[\.\)]\s+(.+)', line)
+ if num_match:
+ flush_ul()
+ ol_items.append(num_match.group(2))
+ elif line.startswith('- ') or line.startswith('* '):
+ flush_ol()
+ ul_items.append(line[2:])
+ else:
+ flush_ol()
+ flush_ul()
+ parts.append(f'{esc(line)}
')
+ flush_ol()
+ flush_ul()
+ return ''.join(parts)
+
+ # Description
description_html = ""
if not is_compliant and rag_desc:
- paras = [p.strip() for p in rag_desc.split('\n\n') if p.strip()]
- if not paras:
- paras = [rag_desc.strip()]
- desc_body = "".join(f'{esc(p)}
' for p in paras)
+ desc_body = _text_to_html_paras(rag_desc)
description_html = f'Description:
{desc_body}'
- # Rationale β split into paragraphs
+ # Rationale
rationale_html = ""
if not is_compliant and rag_rationale:
- paras = [p.strip() for p in rag_rationale.split('\n\n') if p.strip()]
- if not paras:
- paras = [rag_rationale.strip()]
- rat_body = "".join(f'{esc(p)}
' for p in paras)
+ rat_body = _text_to_html_paras(rag_rationale)
rationale_html = f'Rationale:
{rat_body}'
# Remediation β formatted with code snippets
@@ -6079,19 +6396,41 @@ def _generate_compliance_report(rid: str, user_id: str, force_rag: bool = True)
r["__rag_remediation"] = {"description": "", "remediation": ""}
if force_rag:
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
+ import json as _pjson
non_compliant = [r for r in filtered if not r["__compliant"]]
- log.info(f"Compliance report: searching RAG for {len(non_compliant)} non-compliant items")
+ total_rag = len(non_compliant)
+ log.info(f"Compliance report: searching RAG for {total_rag} non-compliant items")
+ progress_marker = rdir / ".compliance_generating"
+
+ def _update_progress(step, current, rec=""):
+ try:
+ progress_marker.write_text(_pjson.dumps({
+ "step": step, "current": current, "total": total_rag, "rec": rec
+ }), encoding="utf-8")
+ except Exception:
+ pass
+
+ _update_progress("RAG", 0)
with ThreadPoolExecutor(max_workers=2) as pool:
- for r in non_compliant:
- try:
- fut = pool.submit(_search_cis_remediation,
- r.get("Title", ""), r.get("Recommendation #", ""), user_id)
- r["__rag_remediation"] = fut.result(timeout=30)
- log.info(f" RAG OK: {r.get('Recommendation #', '?')}")
- except FuturesTimeout:
- log.warning(f" RAG timeout: {r.get('Recommendation #', '?')}")
- except Exception as e:
- log.warning(f" RAG error {r.get('Recommendation #', '?')}: {e}")
+ for idx, r in enumerate(non_compliant):
+ rec_num = r.get('Recommendation #', '?')
+ title = r.get("Title", "")
+ _update_progress("RAG", idx + 1, rec_num)
+ for attempt in range(1, 3):
+ try:
+ fut = pool.submit(_search_cis_remediation, title, rec_num, user_id)
+ r["__rag_remediation"] = fut.result(timeout=60)
+ log.info(f" RAG OK: {rec_num}")
+ break
+ except FuturesTimeout:
+ if attempt == 1:
+ log.warning(f" RAG timeout {rec_num} (attempt 1/2, retrying...)")
+ else:
+ log.warning(f" RAG timeout {rec_num} (attempt 2/2, giving up)")
+ except Exception as e:
+ log.warning(f" RAG error {rec_num}: {e}")
+ break
+ _update_progress("OCI Services", total_rag)
SECTION_ORDER = [
"Identity and Access Management", "Networking", "Compute", "Logging and Monitoring",
@@ -6141,6 +6480,32 @@ def _generate_compliance_report(rid: str, user_id: str, force_rag: bool = True)
# Cache to disk
cache_path = rdir / "compliance_report.html"
cache_path.write_text(html, encoding="utf-8")
+
+ # Save processed data as JSON (used by DOCX generator to produce identical output)
+ import json as _json
+ report_data = {
+ "tenancy_name": tenancy_name,
+ "extract_date": extract_date,
+ "filtered": [{k: v for k, v in r.items() if not k.startswith("__")} | {
+ "__compliant": r["__compliant"],
+ "__pct": r["__pct"],
+ "__findings": r["__findings"],
+ "__rag_remediation": r.get("__rag_remediation", {}),
+ } for r in filtered],
+ "sections": {name: {"total": sd["total"], "passed": sd["passed"], "failed": sd["failed"],
+ "items": [{k: v for k, v in r.items() if not k.startswith("__")} | {
+ "__compliant": r["__compliant"], "__pct": r["__pct"], "__findings": r["__findings"],
+ "__rag_remediation": r.get("__rag_remediation", {}),
+ } for r in sd["items"]]}
+ for name, sd in sections.items()},
+ "oci_services": oci_services,
+ "file_id_map": file_id_map or {},
+ "file_path_map": file_path_map or {},
+ }
+ data_path = rdir / "compliance_data.json"
+ data_path.write_text(_json.dumps(report_data, ensure_ascii=False, default=str), encoding="utf-8")
+ log.info(f"Compliance data cached: {data_path}")
+
log.info(f"Compliance report cached: {cache_path} ({len(html)} bytes)")
return html
@@ -6229,7 +6594,6 @@ async def compliance_report(rid: str, regen: int = Query(0)):
msg = "Generating compliance report... Please wait."
else:
msg = "Compliance report not generated yet."
- from starlette.responses import HTMLResponse
return HTMLResponse(
f''
f'',
@@ -6252,8 +6616,7 @@ async def generate_compliance_report(rid: str, bg: BackgroundTasks, u=Depends(cu
# Reject if already generating
marker = rdir / ".compliance_generating"
if marker.exists():
- import time as _time
- age = _time.time() - marker.stat().st_mtime
+ age = time.time() - marker.stat().st_mtime
if age < 600:
return {"status": "already_generating", "has_rag": False}
@@ -6307,7 +6670,6 @@ async def generate_compliance_report(rid: str, bg: BackgroundTasks, u=Depends(cu
@app.get("/api/reports/{rid}/compliance-report/status")
async def compliance_report_status(rid: str):
"""Check compliance report status: ready, generating, or not started."""
- import time as _time
rdir = REPORTS / rid
cache_path = rdir / "compliance_report.html"
marker = rdir / ".compliance_generating"
@@ -6317,13 +6679,22 @@ async def compliance_report_status(rid: str):
marker.unlink(missing_ok=True)
return {"ready": True, "generating": False}
if marker.exists():
- # Auto-expire marker after 10 minutes (orphaned by restart/crash)
- age = _time.time() - marker.stat().st_mtime
+ age = time.time() - marker.stat().st_mtime
if age > 600:
marker.unlink(missing_ok=True)
log.warning(f"Expired stale compliance marker for {rid} (age={age:.0f}s)")
return {"ready": False, "generating": False}
- return {"ready": False, "generating": True}
+ # Read progress from marker file
+ try:
+ import json as _json
+ progress = _json.loads(marker.read_text(encoding="utf-8"))
+ except Exception:
+ progress = {}
+ return {"ready": False, "generating": True,
+ "current": progress.get("current", 0),
+ "total": progress.get("total", 0),
+ "step": progress.get("step", ""),
+ "rec": progress.get("rec", "")}
return {"ready": False, "generating": False}
@@ -6454,16 +6825,27 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml, OxmlElement
+ import json as _json
rdir = REPORTS / rid
- csv_path = rdir / "cis_summary_report.csv"
- if not csv_path.exists():
+
+ # Read the same processed data used by the HTML report (ensures identical content)
+ data_path = rdir / "compliance_data.json"
+ if not data_path.exists():
+ log.warning(f"DOCX: compliance_data.json not found for {rid} β generate HTML report first")
return None
- with open(csv_path, "r", encoding="utf-8") as f:
- rows = list(csvmod.DictReader(f))
- if not rows:
+ with open(data_path, "r", encoding="utf-8") as f:
+ report_data = _json.load(f)
+
+ tenancy_name = report_data.get("tenancy_name", tenancy)
+ extract_date = report_data.get("extract_date", "")
+ filtered = report_data.get("filtered", [])
+ sections_data = report_data.get("sections", {})
+ oci_services = report_data.get("oci_services", [])
+ file_path_map = report_data.get("file_path_map", file_map)
+
+ if not filtered:
return None
- extract_date = rows[0].get("extract_date", "")
now = _dt.utcnow()
months_en = ["January","February","March","April","May","June","July","August","September","October","November","December"]
month_year = f"{months_en[now.month - 1]}, {now.year}"
@@ -6604,20 +6986,189 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
r.font.name = 'Calibri'
return h
- def _add_body_para(doc, text, size=Pt(10), color=None, bold=False, italic=False, space_after=Pt(4), space_before=Pt(0), align=WD_ALIGN_PARAGRAPH.JUSTIFY):
- """Add a paragraph with consistent body styling."""
+ def _clean_text(text):
+ """Join artificially broken lines into flowing paragraphs.
+ Preserves: paragraph breaks, numbered lists, bullets, headers."""
+ if not text:
+ return text
+ lines = text.split('\n')
+ result = []
+ for line in lines:
+ line = line.strip()
+ if not line:
+ result.append('\n')
+ continue
+ starts_new = (
+ _re.match(r'^\d+[\.\)]\s', line) or
+ _re.match(r'^[a-z][\.\)]\s', line) or
+ line.startswith('- ') or line.startswith('* ') or
+ line.startswith('From ') or
+ line.startswith('Note:') or line.startswith('See ')
+ )
+ if starts_new:
+ result.append(line)
+ elif result and result[-1] != '\n' and not result[-1].endswith(('.', ':', '!', '?', ';')):
+ result[-1] = result[-1] + ' ' + line
+ else:
+ result.append(line)
+ return '\n'.join(result).strip()
+
+ def _add_body_para(doc, text, size=Pt(10), color=None, bold=False, italic=False, space_after=Pt(8), space_before=Pt(0), align=WD_ALIGN_PARAGRAPH.JUSTIFY):
+ """Add paragraph(s) with consistent body styling. Detects numbered/bullet lists and indents them."""
+ text = _clean_text(text)
+ if not text:
+ return None
+ last_p = None
+ for chunk in text.split('\n'):
+ chunk = chunk.strip()
+ if not chunk:
+ continue
+ # Detect list items: "1. text", "a. text", "- text", "* text"
+ list_match = _re.match(r'^(\d+[\.\)]|[a-z][\.\)]|-|\*)\s+(.+)', chunk)
+ p = doc.add_paragraph()
+ p.alignment = WD_ALIGN_PARAGRAPH.LEFT if list_match else align
+ p.paragraph_format.space_before = Pt(1) if list_match else space_before
+ p.paragraph_format.space_after = Pt(3) if list_match else space_after
+ p.paragraph_format.line_spacing = 1.25
+ if list_match:
+ # Indent list items
+ p.paragraph_format.left_indent = Inches(0.3)
+ p.paragraph_format.first_line_indent = Inches(-0.3)
+ # Number/bullet in bold
+ r = p.add_run(list_match.group(1) + ' ')
+ r.font.name = 'Calibri'
+ r.font.size = size
+ r.font.color.rgb = RGBColor(0x2F, 0x55, 0x97)
+ r.bold = True
+ # Content
+ r = p.add_run(list_match.group(2))
+ r.font.name = 'Calibri'
+ r.font.size = size
+ r.font.color.rgb = color or BODY_TEXT
+ else:
+ r = p.add_run(chunk)
+ r.font.name = 'Calibri'
+ r.font.size = size
+ r.font.color.rgb = color or BODY_TEXT
+ r.bold = bold
+ r.italic = italic
+ last_p = p
+ return last_p
+
+ def _is_code_line(line):
+ """Detect if a line is a CLI command or code snippet."""
+ line = line.strip()
+ if not line:
+ return False
+ # Strong indicators: starts with known CLI commands
+ cli_starts = ['oci ', 'aws ', 'az ', 'gcloud ', 'terraform ', 'kubectl ',
+ 'curl ', 'grep ', 'cat ', 'echo ', 'export ', 'sudo ', '#!/']
+ if any(line.startswith(p) for p in cli_starts):
+ return True
+ # Continuation lines (start with --flag or -flag=value)
+ if _re.match(r'^--\w+', line) or _re.match(r'^-\w+=', line):
+ return True
+ # Shell constructs
+ if line in ('do', 'done', 'fi', 'then', 'else', 'esac'):
+ return True
+ if line.startswith('for ') and (' in ' in line or line.endswith('do')):
+ return True
+ return False
+
+ def _is_section_header(line):
+ """Detect "From Console:", "From Command Line:", "Audit / Verification:", etc."""
+ low = line.strip().lower()
+ return (low.startswith('from console') or low.startswith('from oci console') or
+ low.startswith('from command') or low.startswith('from cli') or
+ low.startswith('from the command') or low.startswith('audit') and 'verification' in low)
+
+ def _add_code_block(doc, code_text):
+ """Add a code block with monospace font and gray background."""
p = doc.add_paragraph()
- p.alignment = align
- p.paragraph_format.space_before = space_before
- p.paragraph_format.space_after = space_after
- p.paragraph_format.line_spacing = 1.15
- r = p.add_run(text)
- r.font.name = 'Calibri'
- r.font.size = size
- r.font.color.rgb = color or BODY_TEXT
- r.bold = bold
- r.italic = italic
- return p
+ p.paragraph_format.space_before = Pt(6)
+ p.paragraph_format.space_after = Pt(8)
+ p.paragraph_format.line_spacing = 1.1
+ p.alignment = WD_ALIGN_PARAGRAPH.LEFT
+ pPr = p._p.get_or_add_pPr()
+ pShd = OxmlElement('w:shd')
+ pShd.set(qn('w:val'), 'clear')
+ pShd.set(qn('w:color'), 'auto')
+ pShd.set(qn('w:fill'), 'F3F4F6')
+ pPr.append(pShd)
+ pBdr = OxmlElement('w:pBdr')
+ for edge_name, color in [('left', '9CA3AF'), ('top', 'E5E7EB'), ('bottom', 'E5E7EB'), ('right', 'E5E7EB')]:
+ edge = OxmlElement(f'w:{edge_name}')
+ edge.set(qn('w:val'), 'single')
+ edge.set(qn('w:sz'), '6' if edge_name == 'left' else '4')
+ edge.set(qn('w:space'), '6' if edge_name == 'left' else '2')
+ edge.set(qn('w:color'), color)
+ pBdr.append(edge)
+ pPr.append(pBdr)
+ r = p.add_run(code_text)
+ r.font.name = 'Consolas'
+ r.font.size = Pt(8)
+ r.font.color.rgb = RGBColor(0x1F, 0x29, 0x37)
+
+ def _add_remediation_docx(doc, remediation_text):
+ """Add remediation text with code blocks and section headers, matching HTML output."""
+ # Split into paragraphs first (double newline), then process each
+ paragraphs = _re.split(r'\n{2,}', remediation_text)
+ for para_text in paragraphs:
+ para_text = para_text.strip()
+ if not para_text:
+ continue
+
+ lines = para_text.split('\n')
+
+ # Check if entire paragraph is code
+ non_empty = [l.strip() for l in lines if l.strip()]
+ if non_empty and all(_is_code_line(l) for l in non_empty):
+ _add_code_block(doc, '\n'.join(non_empty))
+ continue
+
+ # Mixed content: process line by line
+ code_buffer = []
+ text_buffer = []
+
+ def flush_code():
+ if code_buffer:
+ _add_code_block(doc, '\n'.join(code_buffer))
+ code_buffer.clear()
+
+ def flush_text():
+ if text_buffer:
+ joined = ' '.join(text_buffer)
+ _add_body_para(doc, joined, size=Pt(9), space_after=Pt(6))
+ text_buffer.clear()
+
+ for line in lines:
+ stripped = line.strip()
+ if not stripped:
+ continue
+
+ if _is_section_header(stripped):
+ flush_text()
+ flush_code()
+ p = doc.add_paragraph()
+ p.paragraph_format.space_before = Pt(8)
+ p.paragraph_format.space_after = Pt(4)
+ r = p.add_run(stripped)
+ r.bold = True
+ r.font.size = Pt(10)
+ r.font.name = 'Calibri'
+ r.font.color.rgb = BODY_TEXT
+ elif _is_code_line(stripped):
+ flush_text()
+ code_buffer.append(stripped)
+ else:
+ flush_code()
+ # If line ends with sentence punctuation, flush as separate paragraph
+ text_buffer.append(stripped)
+ if stripped.endswith(('.', ':', '!', '?', ';')):
+ flush_text()
+
+ flush_code()
+ flush_text()
def _result_box(doc, tenancy_name, is_compliant, findings="0", total="0"):
"""Add a result box with colored background and left border (matching PDF)."""
@@ -6689,12 +7240,14 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
doc.styles['Heading 2'].font.size = Pt(16)
doc.styles['Heading 3'].font.size = Pt(12)
- # ββ Margins ββ
+ # ββ Page size A4 + Margins ββ
sect = doc.sections[0]
- sect.top_margin = Inches(1.0)
- sect.bottom_margin = Inches(0.8)
- sect.left_margin = Inches(1.0)
- sect.right_margin = Inches(1.0)
+ sect.page_width = Cm(21.0)
+ sect.page_height = Cm(29.7)
+ sect.top_margin = Cm(2.5)
+ sect.bottom_margin = Cm(2.0)
+ sect.left_margin = Cm(2.5)
+ sect.right_margin = Cm(2.5)
# ββ Header / Footer ββ
hdr = sect.header
@@ -6703,15 +7256,15 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
hp.alignment = WD_ALIGN_PARAGRAPH.LEFT
r = hp.add_run("ORACLE")
r.font.color.rgb = ORACLE_RED
- r.font.size = Pt(11)
+ r.font.size = Pt(14)
r.bold = True
r.font.name = 'Calibri'
# Add decorative camouflage strip to header (floating, behind text, left edge)
try:
camo_bytes = _generate_camo_strip()
- strip_w = int(0.4 * 914400) # 0.4 inches in EMU
- strip_h = int(11.69 * 914400) # A4 height in EMU
+ strip_w = int(1.0 * 360000) # ~1cm in EMU
+ strip_h = int(29.7 * 360000) # A4 height (297mm) in EMU
_add_floating_image_to_header(hdr, camo_bytes, strip_w, strip_h)
except Exception as e:
log.warning(f"DOCX: failed to add camo strip: {e}")
@@ -6729,18 +7282,18 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
# ββ COVER PAGE ββ
# βββββββββββββββββββββββββββββββββββββββββββββββ
- # Large spacer to push title to middle-bottom of page (matching PDF cover)
- for _ in range(14):
+ # Spacer to push title to center of page
+ for _ in range(10):
sp = doc.add_paragraph()
sp.paragraph_format.space_before = Pt(0)
sp.paragraph_format.space_after = Pt(0)
sp.paragraph_format.line_spacing = 1.0
- # Title β PDF uses serif font (~38pt), light weight, dark color
+ # Title β centered, serif font
p = doc.add_paragraph()
- p.alignment = WD_ALIGN_PARAGRAPH.LEFT
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(0)
- p.paragraph_format.space_after = Pt(2)
+ p.paragraph_format.space_after = Pt(0)
r = p.add_run("CIS Foundations")
r.font.size = Pt(36)
r.font.color.rgb = TITLE_DARK
@@ -6748,28 +7301,36 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
r.bold = False
p = doc.add_paragraph()
- p.alignment = WD_ALIGN_PARAGRAPH.LEFT
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(0)
- p.paragraph_format.space_after = Pt(8)
+ p.paragraph_format.space_after = Pt(12)
r = p.add_run("Benchmark Assessment")
r.font.size = Pt(36)
r.font.color.rgb = TITLE_DARK
r.font.name = 'Georgia'
r.bold = False
- # Subtitle β "{tenancy} β Oracle Cloud Infrastructure"
+ # Subtitle β centered
p = doc.add_paragraph()
- p.alignment = WD_ALIGN_PARAGRAPH.LEFT
- p.paragraph_format.space_before = Pt(12)
- p.paragraph_format.space_after = Pt(16)
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
+ p.paragraph_format.space_before = Pt(8)
+ p.paragraph_format.space_after = Pt(8)
r = p.add_run(f"{tenancy} \u2013 Oracle Cloud Infrastructure")
- r.font.size = Pt(12)
+ r.font.size = Pt(13)
r.font.color.rgb = SUBTITLE_GRAY
r.font.name = 'Calibri'
+ # Extra spacer before metadata
+ for _ in range(6):
+ sp = doc.add_paragraph()
+ sp.paragraph_format.space_before = Pt(0)
+ sp.paragraph_format.space_after = Pt(0)
+ sp.paragraph_format.line_spacing = 1.0
+
# Metadata β identical to PDF cover
def _meta_line(doc, text, bold_prefix=None, sz=Pt(10)):
p = doc.add_paragraph()
+ p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(1)
p.paragraph_format.space_after = Pt(1)
p.paragraph_format.line_spacing = 1.3
@@ -6876,25 +7437,7 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
doc.add_page_break()
# βββββββββββββββββββββββββββββββββββββββββββββββ
- # ββ PROCESS ROWS ββ
- # βββββββββββββββββββββββββββββββββββββββββββββββ
- sections_data = {}
- for r in rows:
- sec = r.get("Section", "")
- if not sec:
- continue
- if sec not in sections_data:
- sections_data[sec] = {"total": 0, "failed": 0, "passed": 0, "items": []}
- sections_data[sec]["total"] += 1
- compliant = (r.get("Compliant", "") or "").strip().lower() in ("yes", "true", "compliant", "ok")
- if compliant:
- sections_data[sec]["passed"] += 1
- else:
- sections_data[sec]["failed"] += 1
- sections_data[sec]["items"].append(r)
-
- # βββββββββββββββββββββββββββββββββββββββββββββββ
- # ββ DOMAINS SUMMARY TABLE ββ
+ # ββ DOMAINS SUMMARY TABLE (data from compliance_data.json β identical to HTML) ββ
# βββββββββββββββββββββββββββββββββββββββββββββββ
_add_heading_red(doc, "CIS Security Assessment", level=1)
_add_body_para(doc, f"Number of controls per domain and non-compliant controls on tenancy {tenancy}:",
@@ -6940,11 +7483,11 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
# βββββββββββββββββββββββββββββββββββββββββββββββ
_add_heading_red(doc, "CIS Benchmark Recommendation Summary", level=1)
bench_data = []
- for r in rows:
+ for r in filtered:
rec = r.get("Recommendation #", "")
title_txt = r.get("Title", "")
level = r.get("Level", "")
- compliant = (r.get("Compliant", "") or "").strip().lower() in ("yes", "true", "compliant", "ok")
+ compliant = r.get("__compliant", False)
st_text = "OK" if compliant else "FAILED"
st_color = GREEN if compliant else RED
bench_data.append((rec, title_txt, f"L{level}" if level else "", (st_text, True, st_color)))
@@ -6960,13 +7503,19 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
_add_heading_red(doc, sec_name, level=1)
for r in sd["items"]:
- compliant = (r.get("Compliant", "") or "").strip().lower() in ("yes", "true", "compliant", "ok")
+ compliant = r.get("__compliant", False)
rec = r.get("Recommendation #", "")
title_txt = r.get("Title", "")
level = r.get("Level", "")
findings = r.get("Findings", "0")
total = r.get("Total", "0")
- remediation = (r.get("Remediation", "") or "").strip()
+ csv_remediation = (r.get("Remediation", "") or "").strip()
+ rag_data = r.get("__rag_remediation", {})
+ if isinstance(rag_data, str):
+ rag_data = {"description": "", "rationale": "", "remediation": rag_data}
+ rag_desc = (rag_data.get("description", "") or "").strip() if isinstance(rag_data, dict) else ""
+ rag_rationale = (rag_data.get("rationale", "") or "").strip() if isinstance(rag_data, dict) else ""
+ rag_rem = (rag_data.get("remediation", "") or "").strip() if isinstance(rag_data, dict) else ""
# Recommendation heading in Oracle Red
_add_heading_red(doc, f"{rec} {title_txt}", level=2)
@@ -6989,32 +7538,29 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
rPr.append(shd)
# Result box with colored background and left border
- _result_box(doc, tenancy, compliant, findings, total)
+ _result_box(doc, tenancy_name, compliant, findings, total)
- # Description section for non-compliant
- if not compliant:
- csv_rem = (r.get("Remediation", "") or "").strip()
- if csv_rem or (findings and total):
- _add_heading_red(doc, "Details:", level=3)
- if findings and total:
- _add_body_para(doc,
- f"Findings: {findings} of {total} items non-compliant",
- size=Pt(9), space_after=Pt(4), align=WD_ALIGN_PARAGRAPH.LEFT)
+ # Description (from RAG β same as HTML)
+ if not compliant and rag_desc:
+ _add_heading_red(doc, "Description:", level=3)
+ _add_body_para(doc, rag_desc, size=Pt(9), space_after=Pt(4))
- # Remediation
+ # Rationale (from RAG β same as HTML)
+ if not compliant and rag_rationale:
+ _add_heading_red(doc, "Rationale:", level=3)
+ _add_body_para(doc, rag_rationale, size=Pt(9), space_after=Pt(4))
+
+ # Remediation (from RAG or CSV β same as HTML, with code blocks)
+ remediation = rag_rem or csv_remediation
if not compliant and remediation:
_add_heading_red(doc, "Remediation:", level=3)
- # Split remediation into paragraphs for readability
- for rem_para in remediation.split('\n\n'):
- rem_para = rem_para.strip()
- if rem_para:
- _add_body_para(doc, rem_para, size=Pt(9), space_after=Pt(4))
+ _add_remediation_docx(doc, remediation)
# Affected Resources table
if not compliant:
csv_filename = (r.get("Filename", "") or "").strip().strip('"').strip("'")
- if csv_filename and csv_filename in file_map:
- csv_file_path = Path(file_map[csv_filename])
+ if csv_filename and csv_filename in file_path_map:
+ csv_file_path = Path(file_path_map[csv_filename])
if csv_file_path.exists():
try:
with open(csv_file_path, "r", encoding="utf-8") as cf:
@@ -7054,35 +7600,16 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
log.warning(f"DOCX: resource table error for {csv_filename}: {e}")
# βββββββββββββββββββββββββββββββββββββββββββββββ
- # ββ OCI SERVICES ββ
+ # ββ OCI SERVICES (from same data as HTML report) ββ
# βββββββββββββββββββββββββββββββββββββββββββββββ
try:
- import json as _json
- with db() as c:
- report_row = c.execute("SELECT config_id FROM reports WHERE id=?", (rid,)).fetchone()
- config_id = report_row["config_id"] if report_row else None
- if config_id:
- detected = []
- try:
- detected = _check_oci_services(config_id)
- except Exception:
- pass
- auto_map = {s["service"]: s for s in detected}
- with db() as c:
- ov_row = c.execute("SELECT value FROM app_settings WHERE key=?", (f"oci_services_{config_id}",)).fetchone()
- overrides = _json.loads(ov_row["value"]) if ov_row else {}
- svc_names = ["Vulnerability Scanning Service", "Data Safe", "Cloud Guard", "Bastion", "Security Zones", "Vault"]
+ if oci_services:
svc_data = []
- svc_full = []
- for name in svc_names:
- ov = overrides.get(name, {})
- au = auto_map.get(name, {"status": "WARNING", "description": "Unable to verify"})
- st = ov.get("status", au["status"])
- desc = ov.get("description", au["description"])
+ for svc in oci_services:
+ st = svc.get("status", "WARNING")
+ desc = svc.get("description", "")
st_color = GREEN if st == "OK" else ORANGE
- svc_data.append(((st, True, st_color), name, desc))
- svc_full.append({"service": name, "status": st, "description": desc,
- "recommendations": au.get("recommendations", [])})
+ svc_data.append(((st, True, st_color), svc["service"], desc))
doc.add_page_break()
_add_heading_red(doc, "OCI SERVICES", level=1)
@@ -7094,7 +7621,7 @@ def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
col_widths=[0.8, 2.0, 3.7])
# Service detail descriptions
- for svc in svc_full:
+ for svc in oci_services:
info = _OCI_SERVICE_DESCRIPTIONS.get(svc["service"], {})
if not info:
continue
@@ -7174,8 +7701,6 @@ async def compliance_report_docx(rid: str, token: str = Query(None), cred: HTTPA
tenancy = report["tenancy_name"] or "report"
safe_name = _re.sub(r'[^\w\-]', '_', tenancy)
try:
- with db() as c:
- files = c.execute("SELECT file_name, file_path FROM report_files WHERE report_id=?", (rid,)).fetchall()
file_map = {f["file_name"]: f["file_path"] for f in files}
docx_bytes = _generate_compliance_docx(rid, tenancy, file_map)
if not docx_bytes:
@@ -7186,7 +7711,6 @@ async def compliance_report_docx(rid: str, token: str = Query(None), cred: HTTPA
except Exception as e:
log.error(f"DOCX generation failed: {e}", exc_info=True)
raise HTTPException(500, f"DOCX generation failed: {e}")
- from starlette.responses import Response
return Response(
content=docx_bytes,
media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document",
@@ -7274,7 +7798,6 @@ async def compliance_report_download(rid: str, token: str = Query(None), cred: H
zf.write(str(p), f"csv/{fname}")
buf.seek(0)
- from starlette.responses import Response
return Response(
content=buf.getvalue(),
media_type="application/zip",
@@ -9297,8 +9820,7 @@ async def startup():
tz_row = c.execute("SELECT value FROM app_settings WHERE key='timezone'").fetchone()
if tz_row and tz_row["value"]:
os.environ["TZ"] = tz_row["value"]
- import time as _time
- _time.tzset()
+ time.tzset()
log.info(f"Timezone set to {tz_row['value']} (from app_settings)")
except Exception as e:
log.warning(f"Failed to set timezone from settings: {e}")
diff --git a/frontend-react/src/pages/ReportsPage.tsx b/frontend-react/src/pages/ReportsPage.tsx
index e88a19b..c7d63de 100644
--- a/frontend-react/src/pages/ReportsPage.tsx
+++ b/frontend-react/src/pages/ReportsPage.tsx
@@ -598,6 +598,7 @@ export default function ReportsPage() {
const setShowCompliance = store.setRptShowCompliance;
const [complianceReady, setComplianceReady] = useState(false);
const [complianceGenerating, setComplianceGenerating] = useState(false);
+ const [complianceProgress, setComplianceProgress] = useState<{ current: number; total: number; step: string; rec: string }>({ current: 0, total: 0, step: '', rec: '' });
/* ββ Error ββ */
const [error, setError] = useState('');
@@ -725,12 +726,16 @@ export default function ReportsPage() {
async () => {
if (!complianceGenerating || !selectedRid) return false;
try {
- const s = await reportsApi.complianceReportStatus(selectedRid);
+ const s = await reportsApi.complianceReportStatus(selectedRid) as { ready: boolean; generating: boolean; current?: number; total?: number; step?: string; rec?: string };
if (s.ready) {
setComplianceReady(true);
setComplianceGenerating(false);
setShowCompliance(true);
- return true; // stop polling
+ setComplianceProgress({ current: 0, total: 0, step: '', rec: '' });
+ return true;
+ }
+ if (s.generating && s.total) {
+ setComplianceProgress({ current: s.current || 0, total: s.total || 0, step: s.step || '', rec: s.rec || '' });
}
} catch { /* keep polling */ }
return false;
@@ -756,6 +761,8 @@ export default function ReportsPage() {
// polling is already running via usePolling above
}, [selectedRid, complianceGenerating]);
+
+
/* ββ Handlers ββ */
const handleRun = async () => {
if (!ociVal) {
@@ -1431,8 +1438,10 @@ export default function ReportsPage() {
src={reportsApi.complianceReportUrl(selectedRid)}
className="w-full border-0"
style={{ height: 700, background: '#fff' }}
- title="LAD A-Team CIS Compliance Report"
+ title="CIS Compliance Report"
/>
+
+ {/* Action buttons */}
) : complianceGenerating ? (
-
-
+
+
{t('rpt.complianceGenerating')}
-
{t('rpt.complianceFetchingKB')}
+ {complianceProgress.total > 0 && (
+
+
+ {complianceProgress.step} {complianceProgress.rec}
+ {complianceProgress.current}/{complianceProgress.total}
+
+
+
+ {Math.round((complianceProgress.current / complianceProgress.total) * 100)}% β Buscando remediaΓ§Γ΅es na base de conhecimento
+
+
+ )}
+ {!complianceProgress.total && (
+
{t('rpt.complianceFetchingKB')}
+ )}
) : (
diff --git a/frontend-react/src/pages/config/EmbeddingsPage.tsx b/frontend-react/src/pages/config/EmbeddingsPage.tsx
index 242e584..367d110 100644
--- a/frontend-react/src/pages/config/EmbeddingsPage.tsx
+++ b/frontend-react/src/pages/config/EmbeddingsPage.tsx
@@ -58,6 +58,17 @@ export default function EmbeddingsPage() {
const [kbUrlUploading, setKbUrlUploading] = useState(false);
const [kbMsg, setKbMsg] = useState(null);
+ // Auto-select ADB config when data loads (fixes empty dropdown after navigation)
+ useEffect(() => {
+ const defaultId = adbCfg.find((c) => c.is_active)?.id || adbCfg[0]?.id || '';
+ if (defaultId) {
+ if (!selVid) setSelVid(defaultId);
+ if (!cisVid) setCisVid(defaultId);
+ if (!kbVid) setKbVid(defaultId);
+ if (!purgeVid) setPurgeVid(defaultId);
+ }
+ }, [adbCfg]); // eslint-disable-line react-hooks/exhaustive-deps
+
// ββ Purge section ββ
const [purgeVid, setPurgeVid] = useState(() => adbCfg.find((c) => c.is_active)?.id || adbCfg[0]?.id || '');
const [purgeTable, setPurgeTable] = useState('');