"""Compliance report generation — HTML builder, RAG remediation, helpers.""" import os, json, re, uuid, time, hashlib from pathlib import Path from datetime import datetime from typing import Optional, List, Dict, Any from fastapi import HTTPException from config import DATA, REPORTS, WALLET_DIR, log, _chat_executor from database import db from auth.jwt_auth import _verify_report_access from services.genai import ( _get_adb_connection, _resolve_embed_config, _embed_text, _DIM_TO_MODEL, _get_table_embedding_dim, _get_active_adb_configs, _get_tables_for_config, _vector_search, _enrich_doc_content, _call_genai, _build_rag_context, ) def _extract_section(text: str, section_name: str) -> str: """Extract a named section from a CIS Benchmark chunk (Description, Remediation, etc.).""" import re as _re pattern = _re.compile(r'\n' + _re.escape(section_name) + r'[:\s]*\n', _re.IGNORECASE) match = pattern.search(text) if not match: return "" start = match.end() end_match = _re.search( r'\n(?:Description|Rationale|Audit|Remediation|Additional Information|CIS Controls|Controls\s*\nVersion|References|Default Value|Impact|Recommendation|Profile Applicability)[:\s]*\n', text[start:], _re.IGNORECASE ) if end_match: return text[start:start + end_match.start()].strip() return text[start:].strip() def _extract_remediation_section(text: str) -> str: """Extract only the Remediation section.""" return _extract_section(text, "Remediation") def _extract_audit_section(text: str) -> str: """Extract the Audit section (verification steps).""" return _extract_section(text, "Audit") def _extract_rationale_section(text: str) -> str: """Extract the Rationale section (why this matters).""" return _extract_section(text, "Rationale") def _extract_description_section(text: str) -> str: """Extract the Description section (what this CIS finding is about).""" return _extract_section(text, "Description") def _format_remediation_html(text: str) -> str: """Format remediation text with code snippets and proper indentation.""" import re as _re if not text: return "" def esc(v): return (v or "").replace("&", "&").replace("<", "<").replace(">", ">").replace('"', """) # Pre-process: join broken lines from PDF extraction # PDF text wraps at column width, creating artificial line breaks mid-sentence processed_lines = [] _BLOCK_STARTERS = _re.compile( r'^(?:\d+\.\s|From |From:|Note:|Remediation:|Audit:|Description:|Rationale:|' r'oci |Allow |Define |for |do$|done$|\$ |query |where |#|' r'Cloud Guard:|Using |Remediation |Default Value|Impact|References|' r'Profile Applicability|Additional Information|CIS Controls)', _re.IGNORECASE ) for line in text.split('\n'): stripped = line.strip() if not stripped: processed_lines.append('') continue prev = processed_lines[-1] if processed_lines else '' # Join if previous line doesn't end with sentence terminator AND # current line is NOT a new block/section/command should_join = ( prev and not prev.endswith(('.', ':', ';', ')', '}', '\\', '|', '"')) and not _BLOCK_STARTERS.match(stripped) ) if should_join: processed_lines[-1] = prev + ' ' + stripped else: processed_lines.append(stripped) text = '\n'.join(processed_lines) # Code line detection patterns _CODE_STARTERS = re.compile( r'^(?:oci |Allow |Define |Endorse |Admit |terraform |curl |openssl |ssh |python|' r'for |do$|done$|if |fi$|echo |export |eval |jq |grep |awk |sed |cat |' r'\$ |#!|output=|[a-z_]+=\$|\{\{|query |where |bucket |' r'select |filter=)', re.IGNORECASE ) _CODE_CONTINUATIONS = re.compile( r'^(?:--[a-z]|[|>]|\)$|\}|&&|\|\||resources\b|\.data|"query |' r'IngressSecurityRules|EgressSecurityRules|tcpOptions|udpOptions|' r'destinationPortRange|sourcePortRange|"region-|"\.region|' r'publicAccessType|ObjectRead|ObjectReadWithoutList|' r'\.source |\.direction |\.protocol |' r'[a-z]+\.[a-z]+\.|2>/dev/null|\()', re.IGNORECASE ) lines = text.split('\n') html_parts = [] in_code = False code_buf = [] def flush_code(): nonlocal in_code, code_buf if code_buf: html_parts.append('
' + esc('\n'.join(code_buf)) + '
') code_buf = [] in_code = False for line in lines: stripped = line.strip() if not stripped: if in_code: code_buf.append('') continue is_code_start = bool(_CODE_STARTERS.match(stripped)) is_code_cont = in_code and ( bool(_CODE_CONTINUATIONS.match(stripped)) or stripped.endswith('\\') or stripped.endswith('|') or stripped.endswith('{') or (not stripped[0].isupper() and not _re.match(r'^\d+\.', stripped) and not stripped.startswith('From ') and not stripped.startswith('To ') and not stripped.startswith('Note') and not stripped.startswith('Ensure') and not stripped.startswith('Cloud ') and not stripped.startswith('The ') and not stripped.startswith('Navigate') and not stripped.startswith('Click') and not stripped.startswith('Select') and not stripped.startswith('In the') and not stripped.startswith('Go to') and not stripped.startswith('Open') and not stripped.startswith('This ') and not stripped.startswith('For ') and not stripped.startswith('If ') and not stripped.startswith('Are ') and not stripped.startswith('Check ') and not stripped.startswith('Verify')) ) if is_code_start or is_code_cont: if not in_code: in_code = True code_buf = [] code_buf.append(stripped) else: flush_code() # "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}

') else: step_match = _re.match(r'^(\d+)\.\s+(.+)', stripped) if step_match: html_parts.append(f'
{step_match.group(1)}. {esc(step_match.group(2))}
') else: html_parts.append(f'

{esc(stripped)}

') flush_code() return '\n'.join(html_parts) def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict: """Search CIS Recommendations in vector DB. Returns {'description': str, 'remediation': str}.""" try: with db() as c: adb_cfgs = c.execute( "SELECT * FROM adb_vector_configs WHERE is_active=1 AND (user_id=? OR is_global=1)", (user_id,) ).fetchall() if not adb_cfgs: return {"description": "", "remediation": ""} query = f"Recommendation: {rec_num} {title} Remediation Audit From Console From Command Line" for adb_cfg in adb_cfgs: adb_cfg = dict(adb_cfg) try: # Resolve embedding config: use the ADB's own embedding model emb_model = adb_cfg.get("embedding_model_id", "cohere.embed-v4.0") tables = _get_tables_for_config(adb_cfg["id"], active_only=True) if not tables: continue # Resolve GenAI config for embedding generation genai_linked = None if adb_cfg.get("genai_config_id"): with db() as c: row = c.execute("SELECT * FROM genai_configs WHERE id=?", (adb_cfg["genai_config_id"],)).fetchone() if row: genai_linked = dict(row) emb_genai = _resolve_embed_config(genai_cfg=genai_linked, user_id=user_id) # Search across cisrecom + section-specific tables all_docs = [] search_tables = [t["table_name"] for t in tables if "cisrecom" in t["table_name"].lower()] if not search_tables: search_tables = [t["table_name"] for t in tables] for tbl_name in search_tables: try: # Detect dimension per table tbl_model = emb_model try: actual_dim = _get_table_embedding_dim(adb_cfg, tbl_name) if actual_dim and actual_dim in _DIM_TO_MODEL: tbl_model = _DIM_TO_MODEL[actual_dim] except Exception as e: log.warning(f"Failed to detect embedding dimension for table '{tbl_name}': {e}") query_embedding = _embed_text(query, emb_genai, tbl_model) docs = _vector_search(adb_cfg, query_embedding, top_k=10, table_name=tbl_name, user_id=user_id) if docs: all_docs.extend(docs) except Exception as e: log.warning(f"Vector search failed for table '{tbl_name}': {e}") if all_docs: import re as _re all_docs.sort(key=lambda d: d.get("distance", 999)) # 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 = [] 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: # 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: extra_rem = _extract_remediation_section(extra) if extra_rem and extra_rem not in rem: rem = rem.rstrip() + "\n\n" + extra_rem if not audit: 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: full_rem = (full_rem + "\n\nAudit / Verification:\n" + audit).strip() if full_rem else audit return {"description": desc, "rationale": rationale, "remediation": full_rem} except Exception as e: log.warning(f"CIS remediation search failed for {adb_cfg.get('config_name','?')}: {e}") return {"description": "", "remediation": ""} except Exception as e: log.warning(f"CIS remediation search error: {e}") return {"description": "", "remediation": ""} def _build_findings_table(csv_path: str, max_rows: int = 10) -> str: """Read a findings CSV and return a compact HTML table with up to max_rows.""" import csv as csvmod, re as _re p = Path(csv_path) if isinstance(csv_path, str) else csv_path if not p.exists(): return "" try: with open(p, "r", encoding="utf-8") as f: all_rows = list(csvmod.DictReader(f)) if not all_rows: return "" def esc(v): return (v or "").replace("&","&").replace("<","<").replace(">",">") def fmt(v, col=""): v = (v or "").strip() if not v: return "—" if v.startswith("=HYPERLINK"): m = _re.search(r',\s*"([^"]+)"', v) return m.group(1) if m else "—" if v.startswith("ocid1.") and len(v) > 30: return v[:25] + "..." + v[-8:] # Format time fields as date + age in days if col.lower() in ("time_created", "time_expires", "last_successful_login_date"): try: from datetime import datetime dt = datetime.fromisoformat(v.replace("Z", "+00:00").split("+")[0].split(".")[0]) age = (datetime.utcnow() - dt).days return f"{v[:10]} ({age}d)" except Exception: return v[:16] # Truncate long values but keep enough to identify if len(v) > 60: return v[:57] + "..." return v # Show all useful columns (skip only internal/redundant ones) all_cols = list(all_rows[0].keys()) skip = {"extract_date", "deep_link", "domain_deeplink", "defined_tags", "domain_id", "external_identifier", "freeform_tags", "system_tags", "email_verified", "is_federated", "api_keys", "auth_tokens", "customer_secret_keys", "database_passwords", "can_use_api_keys", "can_use_auth_tokens", "can_use_console_password", "can_use_customer_secret_keys", "can_use_db_credentials", "can_use_o_auth2_client_credentials", "can_use_smtp_credentials"} display = [c for c in all_cols if c.lower() not in skip] # Limit to max 7 columns to keep table readable if len(display) > 7: # Prioritize: identity cols first, then status, then details priority_order = ["user_name", "name", "display_name", "username", "id", "user_id", "key_id", "lifecycle_state", "status", "is_mfa_activated", "domain_name", "email", "description", "fingerprint", "time_created", "last_successful_login_date", "groups", "compartment_id", "region", "cidr_block", "protocol", "port_range", "visibility", "encryption", "versioning", "age", "time_expires", "statements", "type", "severity"] ordered = [] for p in priority_order: for d in display: if d.lower() == p and d not in ordered: ordered.append(d) # Add any remaining not in priority for d in display: if d not in ordered: ordered.append(d) display = ordered[:7] # Pretty column names _COL_LABELS = { "user_name": "User", "user_id": "User ID", "key_id": "Key ID", "fingerprint": "Fingerprint", "time_created": "Created (Age)", "display_name": "Name", "lifecycle_state": "State", "is_mfa_activated": "MFA", "domain_name": "Domain", "cidr_block": "CIDR", "port_range": "Ports", } def col_label(c): return _COL_LABELS.get(c, c.replace("_", " ").title()) total = len(all_rows) show = all_rows[:max_rows] header = "".join(f'{esc(col_label(c))}' for c in display) rows_html = "" for row in show: cells = "".join(f'{esc(fmt(row.get(c, ""), c))}' for c in display) rows_html += f"{cells}" note = f'
{total} non-compliant resource{"s" if total != 1 else ""}.' if total > max_rows: note += f' Showing first {max_rows} — see CSV for complete list.' note += '
' return f'
{header}{rows_html}
{note}
' except Exception as e: log.warning(f"Failed to build findings table from {p}: {e}") return "" def _check_oci_services(config_id: str) -> list[dict]: """Check status of OCI security services (VSS, Data Safe, Cloud Guard, Bastion, Security Zones, Vault).""" import oci results = [] try: oci_config = _get_oci_config(config_id) tenancy_ocid = oci_config.get("tenancy", "") # Use compartment_id from DB if available, otherwise root tenancy with db() as c: cfg = c.execute("SELECT compartment_id FROM oci_configs WHERE id=?", (config_id,)).fetchone() # DB stores base64 — decode if needed compartment_raw = (cfg["compartment_id"] or "") if cfg else "" if compartment_raw and not compartment_raw.startswith("ocid"): try: compartment_raw = base64.b64decode(compartment_raw).decode("utf-8") except Exception: pass root_compartment = compartment_raw if compartment_raw.startswith("ocid") else tenancy_ocid def _safe_list(resp_data): """Extract list from OCI response data (handles both .items and direct list).""" if hasattr(resp_data, 'items'): return resp_data.items if isinstance(resp_data, list): return resp_data return [] # 1. Vulnerability Scanning Service (VSS) try: vss_client = _oci_client(oci.vulnerability_scanning.VulnerabilityScanningClient, config_id) targets = _safe_list(vss_client.list_host_scan_targets(compartment_id=root_compartment).data) if targets: results.append({"service": "Vulnerability Scanning Service", "status": "OK", "description": f"All instances in the compartment root and its subcompartments are scanned ({len(targets)} target(s))"}) else: results.append({"service": "Vulnerability Scanning Service", "status": "WARNING", "description": "No VSS scan targets found in Tenant"}) except Exception as e: log.warning(f"OCI Services check - VSS: {e}") results.append({"service": "Vulnerability Scanning Service", "status": "WARNING", "description": "Unable to verify VSS status or service not enabled"}) # 2. Data Safe try: ds_client = _oci_client(oci.data_safe.DataSafeClient, config_id) targets = _safe_list(ds_client.list_target_databases(compartment_id=root_compartment, lifecycle_state="ACTIVE").data) if targets: results.append({"service": "Data Safe", "status": "OK", "description": f"{len(targets)} target database(s) registered in Data Safe"}) else: results.append({"service": "Data Safe", "status": "WARNING", "description": "No target databases registered in Data Safe"}) except Exception as e: log.warning(f"OCI Services check - Data Safe: {e}") results.append({"service": "Data Safe", "status": "WARNING", "description": "Unable to verify Data Safe status or service not enabled"}) # 3. Cloud Guard (needs tenancy root) + recommendations try: cg_client = _oci_client(oci.cloud_guard.CloudGuardClient, config_id) cg_config = cg_client.get_configuration(tenancy_ocid).data cg_recommendations = [] if cg_config.status == "ENABLED": try: recs = _safe_list(cg_client.list_recommendations( compartment_id=tenancy_ocid, lifecycle_state="ACTIVE", sort_by="riskLevel", sort_order="DESC", limit=50).data) cg_recommendations = [{"name": r.name, "count": r.problem_count, "risk": r.risk_level} for r in recs if r.problem_count and r.problem_count > 0] except Exception as re: log.warning(f"Cloud Guard recommendations fetch failed: {re}") results.append({"service": "Cloud Guard", "status": "OK", "description": "Cloud Guard service is enabled in Tenant", "recommendations": cg_recommendations}) else: results.append({"service": "Cloud Guard", "status": "WARNING", "description": "Cloud Guard service is not enabled in Tenant"}) except Exception as e: log.warning(f"OCI Services check - Cloud Guard: {e}") results.append({"service": "Cloud Guard", "status": "WARNING", "description": "Unable to verify Cloud Guard status"}) # 4. Bastion try: bastion_client = _oci_client(oci.bastion.BastionClient, config_id) bastions = _safe_list(bastion_client.list_bastions(compartment_id=root_compartment).data) if bastions: results.append({"service": "Bastion", "status": "OK", "description": f"Some OCI Bastion instances were found in Tenant ({len(bastions)})"}) else: results.append({"service": "Bastion", "status": "WARNING", "description": "No OCI Bastion instances found in Tenant"}) except Exception as e: log.warning(f"OCI Services check - Bastion: {e}") results.append({"service": "Bastion", "status": "WARNING", "description": "Unable to verify Bastion status"}) # 5. Security Zones (needs tenancy root) try: cg_client_sz = _oci_client(oci.cloud_guard.CloudGuardClient, config_id) sz = _safe_list(cg_client_sz.list_security_zones(compartment_id=tenancy_ocid).data) if sz: results.append({"service": "Security Zones", "status": "OK", "description": f"Security Zones were created in Tenant ({len(sz)})"}) else: results.append({"service": "Security Zones", "status": "WARNING", "description": "No Security Zones found in Tenant"}) except Exception as e: log.warning(f"OCI Services check - Security Zones: {e}") results.append({"service": "Security Zones", "status": "WARNING", "description": "Unable to verify Security Zones status"}) # 6. Vault try: vault_client = _oci_client(oci.key_management.KmsVaultClient, config_id) vaults = _safe_list(vault_client.list_vaults(compartment_id=root_compartment).data) active_vaults = [v for v in vaults if v.lifecycle_state == "ACTIVE"] if active_vaults: results.append({"service": "Vault", "status": "OK", "description": f"Vault instances were found in Tenant ({len(active_vaults)} active)"}) else: results.append({"service": "Vault", "status": "WARNING", "description": "No active Vault instances found in Tenant"}) except Exception as e: log.warning(f"OCI Services check - Vault: {e}") results.append({"service": "Vault", "status": "WARNING", "description": "Unable to verify Vault status"}) except Exception as e: log.error(f"OCI Services check failed: {e}") return results # ── Static descriptions for OCI Services section ── _OCI_SERVICE_DESCRIPTIONS = { "Vulnerability Scanning Service": { "title": "Vulnerability Scanning Service", "description": ( "Oracle Cloud Infrastructure Vulnerability Scanning Service (OCI VSS) is simple, prescriptive, and tightly integrated " "with the OCI platform. VSS is available to all OCI customers that have paid accounts at no additional cost. The " "scanning platform includes default plugins and engines for instance and container scanning. The service scans " "installed packages and artifacts looking for the existence of known vulnerabilities. The service scans for open ports " "on an instance and it reports on publicly and privately available open ports. These findings will highlight what ports " "an attacker might use or what application could be shut down by the customer to reduce their attack surface. The " "scanning agent also reviews the configuration of each instance against specific OS CIS benchmarks enabling " "customers to see immediately what Operating System (OS) security hardening opportunities exist on their instances." ), "recommendation": "Create a recipe for scanning instances/containers and targets to use those recipes. Collect all results in Cloud Guard.", }, "Data Safe": { "title": "Data Safe", "description": ( "An audit trail represents the collection of audit records from the target database such as " "UNIFIED_AUDIT_TRAIL, which provides documentary evidence of the sequence of activities that happen. " "A database audit trail is the source of audit records showing what has happened in the target database. When audit " "data collection is enabled for the specified database audit trail in an audit trail resource, the audit records are " "copied from the database's audit trail into Oracle Data Safe in near-real time. You can manage the audit records " "volume in the target database using the auto purge feature." ), "recommendation": ( "Start Oracle Data Safe audit trail for all Databases. " "Take advantage of Activity Auditing Dashboard and Audit Insights." ), }, "Cloud Guard": { "title": "Cloud Guard", "description": ( "Misconfigured resources and insecure activity in the cloud represent one of the most difficult problems for security " "professionals. Misconfigured cloud tenancies around cloud resources can present themselves in many ways; from " "publicly accessible object storage buckets, unencrypted data storage, sensitive ports open to the internet. " "Oracle Cloud Infrastructure Cloud Guard is a cloud security service that detects misconfigured resources and " "insecure activities. Cloud Guard acts as a log and events aggregator that directly integrates with all major Oracle " "Cloud Infrastructure services (Compute, Networking, Storage, etc.) providing actionable results. Cloud Guard offers " "the flexibility to take action on security issues manually or automatically with conditional operators." ), "recommendation": "Based on its monitoring (aligned with CIS baselines), Cloud Guard provides in its console recommendations, some already highlighted in this document.", }, "Bastion": { "title": "OCI Bastion", "description": ( "Provide restricted and time-limited secure access to resources that don't have public endpoints and require strict " "resource access controls. Examples include compute instances, bare metal and virtual machines, MySQL, ATP, OKE, " "and any other resource that allows Secure Shell Protocol (SSH) access. With Oracle Cloud Infrastructure (OCI) " "Bastion service, customers can enable access to private hosts without deploying and maintaining a jump host. In " "addition, customers gain improved security posture with identity-based permissions and a centralized, audited, and " "time-bound SSH session. OCI Bastion removes the need for a public IP for bastion access, eliminating the hassle and " "potential attack surface from remote access." ), "recommendation": "We recommend using Bastion to remediate findings from CIS Controls 2.1, 2.2, 2.3 and 2.4 in public subnets, if the permission is related to remote access use cases without restricted sources, VPN or Fast Connect.", }, "Security Zones": { "title": "Security Zones", "description": ( "Security Zones let you be confident that your resources in Oracle Cloud Infrastructure, including Compute, " "Networking, Object Storage, Block Volume and Database resources, comply with your security policies. " "A security zone is associated with one or more compartments and a security zone recipe. When you create and " "update resources in a security zone, Oracle Cloud Infrastructure validates these operations against the list of " "policies that are defined in the security zone recipe. If any security zone policy is violated, then the operation is " "denied. By default, a compartment and any subcompartments are in the same security zone, but you can also create " "a different security zone for a subcompartment." ), "recommendation": "", }, "Vault": { "title": "Vault", "description": ( "Vaults are logical entities where Vault service creates and durably stores vault keys and secrets. The type of vault " "you have determines features and functionality such as degrees of storage isolation, access to management and " "encryption, scalability, and the ability to back up. The type of vault you have also affects pricing. You cannot change " "a vault's type after you create the vault. " "The Vault service offers different vault types to accommodate your organization's needs and budget. All vault types " "ensure the security and integrity of the encryption keys and secrets that vaults store. A virtual private vault is an " "isolated partition on a hardware security module (HSM). Vaults otherwise share partitions on the HSM with other vaults." ), "recommendation": "", }, } def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date: str, sections_data: dict, rid: str = "", file_id_map: dict = None, file_path_map: dict = None, oci_services: list = None) -> str: """Build the full LAD A-Team CIS Compliance Report HTML.""" import re as _re from datetime import datetime as _dt now = _dt.utcnow() months_pt = ["Janeiro","Fevereiro","Março","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"] month_year = f"{months_pt[now.month - 1]}, {now.year}" 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 pct_global = round(total_passed / total_all * 100) if total_all else 0 # ── Domains table rows ── domains_rows = "" for sec_name, sd in sections_data.items(): domains_rows += f'{esc(sec_name)}{sd["total"]}{sd["failed"]}{sd["passed"]}' # ── CIS Benchmark Summary table ── summary_rows = "" current_section = "" for r in filtered_rows: sec = r["Section"] rec = r.get("Recommendation #", "") title = r.get("Title", "") level = (r.get("Level", "") or "").strip() is_compliant = r["__compliant"] if sec != current_section: current_section = sec summary_rows += f'{esc(sec)}' status_cls = "pass" if is_compliant else "fail" status_txt = "OK" if is_compliant else "FAILED" level_txt = f'L{level}' if level else '' summary_rows += f'{esc(rec)}{esc(title)}{level_txt}{status_txt}' # ── Detailed sections ── sections_html = "" for sec_name, sd in sections_data.items(): sec_id = f"section-{slug(sec_name)}" sections_html += f'
' sections_html += '
ORACLE
' sections_html += f'

{esc(sec_name)}

' for r in sd["items"]: rec = r.get("Recommendation #", "") title = r.get("Title", "") level = (r.get("Level", "") or "").strip() cis_v8 = (r.get("CIS v8", "") or "").strip().strip("[]'\"") cccs = (r.get("CCCS Guard Rail", "") or "").strip().strip("[]'\"") rec_id = f"rec-{slug(sec_name)}-{slug(rec)}" is_compliant = r["__compliant"] findings = r.get("Findings", "").strip() findings_num = r["__findings"] total_items = r.get("Total", "").strip() compliant_items = r.get("Compliant Items", "").strip() pct = r["__pct"] 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() rag_rationale = (rag_data.get("rationale", "") or "").strip() rag_rem = (rag_data.get("remediation", "") or "").strip() # Status line if is_compliant: result_html = f'
Result: Tenancy {esc(tenancy_name)}: Compliant
' else: detail = "" if findings_num is not None and total_items: detail = f" ({findings_num} of {esc(total_items)} items)" result_html = f'
Result: Tenancy {esc(tenancy_name)}: Non-Compliant{detail}
' def _text_to_html_paras(text): """Convert text to HTML paragraphs, rendering numbered lists as
    and bullets as
' # ── OCI Services section ── oci_services_html = "" if oci_services: svc_rows = "" for svc in oci_services: st = svc["status"] st_cls = "pass" if st == "OK" else "warn" svc_rows += f'{esc(st)}{esc(svc["service"])}{esc(svc["description"])}' # Build service detail pages (each pair of services on a new page with ORACLE header) svc_detail_pages = "" svc_list = [s for s in oci_services if _OCI_SERVICE_DESCRIPTIONS.get(s["service"])] for idx, svc in enumerate(svc_list): svc_name = svc["service"] info = _OCI_SERVICE_DESCRIPTIONS[svc_name] # Start new page every 2 services (or first one) if idx % 2 == 0: if idx > 0: svc_detail_pages += '' # close previous page div svc_detail_pages += f'
' svc_detail_pages += '
ORACLE
' svc_detail_pages += f'
' svc_detail_pages += f'

{esc(info.get("title", svc_name))}

' desc = info.get("description", "") for para in desc.split("\n"): para = para.strip() if para: svc_detail_pages += f'

{esc(para)}

' rec = info.get("recommendation", "") if rec: svc_detail_pages += f'

Recommendation:

{esc(rec)}

' cg_recs = svc.get("recommendations", []) if cg_recs: svc_detail_pages += '' svc_detail_pages += '' for cr in cg_recs: svc_detail_pages += f'' svc_detail_pages += '
RecommendationsTotal
{esc(cr["name"])}{cr["count"]}
' svc_detail_pages += '
' if svc_list: svc_detail_pages += '
' # close last page div oci_services_html = f'''
ORACLE

OCI Services

Recommendations:

The recommended services are already enabled. This way we recommend further development of their usage:

{svc_rows}
STATUSSERVICEDescription
{svc_detail_pages}''' # ── TOC ── toc_lines = '
CIS Security Assessment Summary
' toc_lines += '
CIS Benchmark Recommendation Summary
' for sec_name, sd in sections_data.items(): sec_id = f"section-{slug(sec_name)}" toc_lines += f'
{esc(sec_name)}{sd["total"]}
' for r in sd["items"]: rec = r.get("Recommendation #", "-") title = r.get("Title", "-") rec_id = f"rec-{slug(sec_name)}-{slug(rec)}" toc_lines += f'
{esc(rec)} {esc(title)}
' if oci_services: toc_lines += '
OCI Services
' DISCLAIMER_TEXT = "This document provides prescriptive guidance for establishing a secure baseline configuration for the Oracle Cloud Infrastructure environment based on the CIS Oracle Cloud Infrastructure Foundations Benchmark. The scope of this benchmark is to establish a base level of security for anyone utilizing the included Oracle Cloud Infrastructure services. The benchmark is, however, not an exhaustive list of all possible security configurations and architectures. You should take the benchmark as a starting point and do the required site-specific tailoring wherever needed and when it is prudent to do so.\n\nThis document is provided \"as is\" for informational purposes only, without warranties of any kind, whether express or implied. The findings and recommendations reflect the conditions observed at the time of the assessment and do not guarantee the prevention of future security incidents or compliance outcomes. This document does not form part of any Oracle license, service contract, or other agreement." PROFILE_DEFINITIONS = """Level 1 — Items in this profile intend to be practical and prudent, provide a clear security benefit, and not inhibit the utility of the technology beyond acceptable means. Level 2 — Items in this profile extend the Level 1 profile. Items in this profile exhibit one or more of the following characteristics: are intended for environments or use cases where security is paramount, act as defense in depth measure, and may negatively inhibit the utility or performance of the technology.""" SECURITY_OVERVIEW = """Oracle's mission is to build cloud infrastructure and platform services for your business to have effective and manageable security to run your mission-critical workloads and store your data with confidence. Oracle Cloud Infrastructure's security approach is based on seven core pillars: Customer Isolation — Allow customers to deploy their application and data assets in an environment that commits full isolation from other tenants and Oracle's staff. Data Encryption — Protect customer data at-rest and in-transit in a way that allows customers to meet their security and compliance requirements for cryptographic algorithms and key management. Security Controls — Offer customers effective and easy-to-use security management solutions that allow them to constrain access to their services and segregate operational responsibilities. Visibility — Offer customers comprehensive log data and security analytics that they can use to audit and monitor actions on their resources. Secure Hybrid Cloud — Enable customers to use their existing security assets, such as user accounts and policies, as well as third-party security solutions when accessing their cloud resources. High Availability — Offer fault-independent data centers that enable high availability scale-out architectures and are resilient against network attacks. Verifiably Secure Infrastructure — Follow rigorous processes and use effective security controls in all phases of cloud service development and operation.""" return f''' CIS Foundations Benchmark Assessment — {esc(tenancy_name)}
''' def _generate_compliance_report(rid: str, user_id: str, force_rag: bool = True) -> str: """Generate and cache the LAD A-Team CIS Compliance Report HTML.""" import csv as csvmod import re as _re with db() as c: report = c.execute("SELECT * FROM reports WHERE id=?", (rid,)).fetchone() if not report: raise HTTPException(404, "Report not found") rdir = REPORTS / rid csv_path = rdir / "cis_summary_report.csv" if not csv_path.exists(): raise HTTPException(404, "cis_summary_report.csv not found") rows = [] with open(csv_path, "r", encoding="utf-8") as f: for row in csvmod.DictReader(f): rows.append(row) if not rows: raise HTTPException(404, "No data in summary CSV") tenancy_name = report["tenancy_name"] or "Unknown" extract_date = rows[0].get("extract_date", "") if rows else "" # Build filename → file_id and filename → file_path maps file_id_map = {} file_path_map = {} with db() as c: rf = c.execute("SELECT id, file_name, file_path FROM report_files WHERE report_id=?", (rid,)).fetchall() for f in rf: file_id_map[f["file_name"]] = f["id"] file_path_map[f["file_name"]] = f["file_path"] def norm_section(raw): s = _re.sub(r"\s+", " ", (raw or "").strip()) if not s: return None if _re.match(r"^Logging and Monitoring\s*\d*$", s, _re.I): return "Logging and Monitoring" if _re.match(r"^Storage$", s, _re.I): return None if _re.match(r"^Storage\s*-\s*Block Volumes$", s, _re.I): return "Storage - Block Volumes" if _re.match(r"^Storage\s*-\s*File Storage Service$", s, _re.I): return "Storage - File Storage Service" if _re.match(r"^Storage\s*-\s*Object Storage$", s, _re.I): return "Storage - Object Storage" return s seen = set() filtered = [] for r in rows: sec = norm_section(r.get("Section", "")) if not sec: continue key = f"{sec}||{r.get('Recommendation #', '')}||{r.get('Title', '')}".lower() if key in seen: continue seen.add(key) r["Section"] = sec status_raw = (r.get("Compliant", "") or "").strip().lower() r["__compliant"] = status_raw in ("yes", "true", "compliant", "ok") pct_str = (r.get("Compliance Percentage Per Recommendation", "") or "").replace(",", ".") m = _re.search(r"(\d+(\.\d+)?)", pct_str) r["__pct"] = max(0, min(100, float(m.group(1)))) if m else None f_str = (r.get("Findings", "") or "").replace(",", "") fm = _re.search(r"\d+", f_str) r["__findings"] = int(fm.group(0)) if fm else None filtered.append(r) # RAG remediations (with per-item timeout) for r in filtered: 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"]] 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 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", "Storage - Object Storage", "Storage - Block Volumes", "Storage - File Storage Service", "Asset Management", ] from collections import OrderedDict sections = OrderedDict() for sec in SECTION_ORDER: items = [r for r in filtered if r["Section"] == sec] if items: items.sort(key=lambda x: [int(p) if p.isdigit() else 0 for p in (x.get("Recommendation #", "0") or "0").split(".")]) passed = sum(1 for x in items if x["__compliant"]) sections[sec] = {"items": items, "total": len(items), "passed": passed, "failed": len(items) - passed} # Check OCI security services status (with user overrides) oci_services = [] config_id = report["config_id"] if report else None if config_id: try: import json as _json log.info(f"Compliance report: checking OCI security services status...") detected = _check_oci_services(config_id) # Load user overrides 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 {} # Merge: build final list with overrides taking priority auto_map = {s["service"]: s for s in detected} for name in ["Vulnerability Scanning Service", "Data Safe", "Cloud Guard", "Bastion", "Security Zones", "Vault"]: ov = overrides.get(name, {}) au = auto_map.get(name, {"service": name, "status": "WARNING", "description": "Unable to verify"}) svc = { "service": name, "status": ov.get("status", au["status"]), "description": ov.get("description", au["description"]), } if "recommendations" in au: svc["recommendations"] = au["recommendations"] oci_services.append(svc) log.info(f"Compliance report: OCI services check done ({len(oci_services)} services, {len(overrides)} overrides)") except Exception as e: log.warning(f"OCI services check failed: {e}") html = _build_compliance_html(filtered, tenancy_name, extract_date, sections, rid, file_id_map, file_path_map, oci_services) # 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