Fase 0 complete: extract all business logic, auth, database, and 176 endpoints from monolithic app.py into dedicated modules. Structure: - app.py: FastAPI hub (CORS, routers, startup/shutdown) - models.py: 13 Pydantic request models - utils.py: shared utilities (embed status, upload validation, process registries) - config.py: constants, env vars, model catalogs - auth/: crypto, jwt_auth, oidc, rate_limit - database/: db(), init_db(), schema DDL - services/: genai, compliance, chat, terraform, embeddings, cis_reports, mcp - routes/: 13 APIRouter modules (auth, users, oci_config, oci_explorer, genai, mcp, adb, embeddings, reports, chat, terraform, settings, cis_engine) Also: README updated with OCIR auth instructions for manual docker run. 86 tests passing.
1430 lines
77 KiB
Python
1430 lines
77 KiB
Python
"""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('<pre class="code-snippet">' + esc('\n'.join(code_buf)) + '</pre>')
|
||
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'<p class="rem-text"><b>{esc(header_match.group(1).strip())}</b> {rest}</p>')
|
||
else:
|
||
step_match = _re.match(r'^(\d+)\.\s+(.+)', stripped)
|
||
if step_match:
|
||
html_parts.append(f'<div class="rem-step"><span class="step-num">{step_match.group(1)}.</span> {esc(step_match.group(2))}</div>')
|
||
else:
|
||
html_parts.append(f'<p class="rem-text">{esc(stripped)}</p>')
|
||
|
||
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'<th>{esc(col_label(c))}</th>' for c in display)
|
||
rows_html = ""
|
||
for row in show:
|
||
cells = "".join(f'<td>{esc(fmt(row.get(c, ""), c))}</td>' for c in display)
|
||
rows_html += f"<tr>{cells}</tr>"
|
||
|
||
note = f'<div class="findings-note">{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 += '</div>'
|
||
|
||
return f'<div class="findings-table-wrap"><table class="findings-table"><thead><tr>{header}</tr></thead><tbody>{rows_html}</tbody></table>{note}</div>'
|
||
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'<tr><td class="dom">{esc(sec_name)}</td><td class="num">{sd["total"]}</td><td class="num fail">{sd["failed"]}</td><td class="num pass">{sd["passed"]}</td></tr>'
|
||
|
||
# ── 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'<tr class="section-header"><td colspan="4"><b>{esc(sec)}</b></td></tr>'
|
||
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'<tr><td class="rec-num">{esc(rec)}</td><td>{esc(title)}</td><td class="num" style="font-size:9px;color:var(--muted)">{level_txt}</td><td class="num {status_cls}">{status_txt}</td></tr>'
|
||
|
||
# ── Detailed sections ──
|
||
sections_html = ""
|
||
for sec_name, sd in sections_data.items():
|
||
sec_id = f"section-{slug(sec_name)}"
|
||
sections_html += f'<div class="detail-section page-break-before" id="{esc(sec_id)}">'
|
||
sections_html += '<div class="oracle-brand">ORACLE</div>'
|
||
sections_html += f'<h1 class="section-num">{esc(sec_name)}</h1>'
|
||
|
||
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'<div class="result-box ok"><b>Result:</b> Tenancy {esc(tenancy_name)}: <span class="pass-text">Compliant</span></div>'
|
||
else:
|
||
detail = ""
|
||
if findings_num is not None and total_items:
|
||
detail = f" ({findings_num} of {esc(total_items)} items)"
|
||
result_html = f'<div class="result-box fail"><b>Result:</b> Tenancy {esc(tenancy_name)}: <span class="fail-text">Non-Compliant{detail}</span></div>'
|
||
|
||
def _text_to_html_paras(text):
|
||
"""Convert text to HTML paragraphs, rendering numbered lists as <ol> and bullets as <ul>."""
|
||
cleaned = _join_broken_lines(text)
|
||
parts = []
|
||
lines = cleaned.split('\n')
|
||
ol_items = []
|
||
ul_items = []
|
||
def flush_ol():
|
||
if ol_items:
|
||
parts.append('<ol style="margin:4px 0;padding-left:24px">' + ''.join(f'<li>{esc(i)}</li>' for i in ol_items) + '</ol>')
|
||
ol_items.clear()
|
||
def flush_ul():
|
||
if ul_items:
|
||
parts.append('<ul style="margin:4px 0;padding-left:24px">' + ''.join(f'<li>{esc(i)}</li>' for i in ul_items) + '</ul>')
|
||
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'<p>{esc(line)}</p>')
|
||
flush_ol()
|
||
flush_ul()
|
||
return ''.join(parts)
|
||
|
||
# Description
|
||
description_html = ""
|
||
if not is_compliant and rag_desc:
|
||
desc_body = _text_to_html_paras(rag_desc)
|
||
description_html = f'<div class="finding-desc"><h4>Description:</h4>{desc_body}</div>'
|
||
|
||
# Rationale
|
||
rationale_html = ""
|
||
if not is_compliant and rag_rationale:
|
||
rat_body = _text_to_html_paras(rag_rationale)
|
||
rationale_html = f'<div class="finding-rationale"><h4>Rationale:</h4>{rat_body}</div>'
|
||
|
||
# Remediation — formatted with code snippets
|
||
remediation_html = ""
|
||
if not is_compliant:
|
||
rem_text = rag_rem or csv_remediation
|
||
if rem_text:
|
||
formatted = _format_remediation_html(rem_text)
|
||
remediation_html = f'<div class="remediation"><h4>Remediation:</h4><div class="remediation-content">{formatted}</div></div>'
|
||
|
||
# Compliance bar
|
||
bar_html = ""
|
||
if pct is not None:
|
||
cls = "bad" if pct <= 20 else "warn" if pct <= 60 else "ok"
|
||
bar_html = f'<div class="compliance-bar"><span class="bar-label">{pct:.0f}% Compliant</span><div class="bar"><div class="fill {cls}" style="width:{pct:.0f}%"></div></div></div>'
|
||
|
||
# Findings file link + resource table
|
||
findings_file_html = ""
|
||
findings_table_html = ""
|
||
csv_filename = (r.get("Filename", "") or "").strip().strip('"').strip("'")
|
||
if csv_filename and not is_compliant:
|
||
if file_id_map:
|
||
fid = file_id_map.get(csv_filename, "")
|
||
if fid and rid:
|
||
dl_url = f"/api/reports/{rid}/files/{fid}/download"
|
||
findings_file_html = f'<div class="findings-file"><a href="{dl_url}" target="_blank" class="file-link">[CSV] {esc(csv_filename)}</a><span class="file-hint">Affected resources details</span></div>'
|
||
if file_path_map:
|
||
csv_path = file_path_map.get(csv_filename, "")
|
||
if csv_path:
|
||
findings_table_html = _build_findings_table(csv_path, max_rows=10)
|
||
|
||
level_badge = ""
|
||
if level:
|
||
lvl_cls = "level-1" if level == "1" else "level-2"
|
||
level_badge = f'<span class="level-badge {lvl_cls}">Level {esc(level)}</span>'
|
||
refs_html = ""
|
||
ref_parts = []
|
||
if cis_v8: ref_parts.append(f"CIS v8: {esc(cis_v8)}")
|
||
if cccs: ref_parts.append(f"CCCS: {esc(cccs)}")
|
||
if ref_parts:
|
||
refs_html = f'<div class="finding-refs">{" | ".join(ref_parts)}</div>'
|
||
|
||
sections_html += f'''
|
||
<div class="finding" id="{esc(rec_id)}">
|
||
<h2>{esc(rec)} {esc(title)} {level_badge}</h2>
|
||
{refs_html}
|
||
{bar_html}
|
||
{result_html}
|
||
{findings_table_html}
|
||
{findings_file_html}
|
||
{description_html}
|
||
{rationale_html}
|
||
{remediation_html}
|
||
</div>'''
|
||
|
||
sections_html += '</div>'
|
||
|
||
# ── 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'<tr><td class="num {st_cls}">{esc(st)}</td><td><b>{esc(svc["service"])}</b></td><td>{esc(svc["description"])}</td></tr>'
|
||
|
||
# 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 += '</div>' # close previous page div
|
||
svc_detail_pages += f'<div class="detail-section page-break-before">'
|
||
svc_detail_pages += '<div class="oracle-brand">ORACLE</div>'
|
||
|
||
svc_detail_pages += f'<div class="service-detail">'
|
||
svc_detail_pages += f'<h2 style="color:#c74634;margin-top:28px">{esc(info.get("title", svc_name))}</h2>'
|
||
desc = info.get("description", "")
|
||
for para in desc.split("\n"):
|
||
para = para.strip()
|
||
if para:
|
||
svc_detail_pages += f'<p>{esc(para)}</p>'
|
||
rec = info.get("recommendation", "")
|
||
if rec:
|
||
svc_detail_pages += f'<p><b>Recommendation:</b></p><p>{esc(rec)}</p>'
|
||
cg_recs = svc.get("recommendations", [])
|
||
if cg_recs:
|
||
svc_detail_pages += '<table class="benchmark" style="margin-top:12px;margin-bottom:12px;font-size:9px">'
|
||
svc_detail_pages += '<thead><tr><th style="text-align:left">Recommendations</th><th style="width:60px;text-align:right">Total</th></tr></thead><tbody>'
|
||
for cr in cg_recs:
|
||
svc_detail_pages += f'<tr><td>{esc(cr["name"])}</td><td style="text-align:right;font-weight:700">{cr["count"]}</td></tr>'
|
||
svc_detail_pages += '</tbody></table>'
|
||
svc_detail_pages += '</div>'
|
||
if svc_list:
|
||
svc_detail_pages += '</div>' # close last page div
|
||
|
||
oci_services_html = f'''
|
||
<div class="detail-section page-break-before" id="oci-services">
|
||
<div class="oracle-brand">ORACLE</div>
|
||
<h1 class="section-num" style="color:#c74634;text-transform:uppercase">OCI Services</h1>
|
||
<h2 style="color:#c74634">Recommendations:</h2>
|
||
<p>The recommended services are already enabled. This way we recommend further development of their usage:</p>
|
||
<table class="benchmark" style="margin-top:12px;margin-bottom:24px">
|
||
<thead><tr><th style="width:80px;text-align:center">STATUS</th><th style="width:200px">SERVICE</th><th>Description</th></tr></thead>
|
||
<tbody>{svc_rows}</tbody>
|
||
</table>
|
||
</div>
|
||
{svc_detail_pages}'''
|
||
|
||
# ── TOC ──
|
||
toc_lines = '<div class="toc-line toc-l0"><a href="#cis-summary">CIS Security Assessment Summary</a></div>'
|
||
toc_lines += '<div class="toc-line toc-l0"><a href="#benchmark-table">CIS Benchmark Recommendation Summary</a></div>'
|
||
for sec_name, sd in sections_data.items():
|
||
sec_id = f"section-{slug(sec_name)}"
|
||
toc_lines += f'<div class="toc-line toc-l0"><a href="#{sec_id}">{esc(sec_name)}</a><span class="toc-count">{sd["total"]}</span></div>'
|
||
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'<div class="toc-line toc-l1"><a href="#{rec_id}">{esc(rec)} {esc(title)}</a></div>'
|
||
if oci_services:
|
||
toc_lines += '<div class="toc-line toc-l0"><a href="#oci-services">OCI Services</a></div>'
|
||
|
||
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 = """<b>Level 1</b> — 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.
|
||
|
||
<b>Level 2</b> — 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:
|
||
|
||
<b>Customer Isolation</b> — Allow customers to deploy their application and data assets in an environment that commits full isolation from other tenants and Oracle's staff.
|
||
|
||
<b>Data Encryption</b> — 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.
|
||
|
||
<b>Security Controls</b> — Offer customers effective and easy-to-use security management solutions that allow them to constrain access to their services and segregate operational responsibilities.
|
||
|
||
<b>Visibility</b> — Offer customers comprehensive log data and security analytics that they can use to audit and monitor actions on their resources.
|
||
|
||
<b>Secure Hybrid Cloud</b> — 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.
|
||
|
||
<b>High Availability</b> — Offer fault-independent data centers that enable high availability scale-out architectures and are resilient against network attacks.
|
||
|
||
<b>Verifiably Secure Infrastructure</b> — Follow rigorous processes and use effective security controls in all phases of cloud service development and operation."""
|
||
|
||
return f'''<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<title>CIS Foundations Benchmark Assessment — {esc(tenancy_name)}</title>
|
||
<style>
|
||
@page {{ size: A4; margin: 0; }}
|
||
@media screen {{
|
||
.print-header-space, .print-footer-space {{ display: none; }}
|
||
}}
|
||
@media print {{
|
||
.no-print {{ display: none !important; }}
|
||
body {{ background: #fff !important; margin: 0; padding: 0; }}
|
||
html {{ margin: 0; padding: 0; }}
|
||
.wrap {{ margin-left: calc(var(--strip-w) + 10px); padding: 0 14mm 0 14px; max-width: none; }}
|
||
.page-strip {{ width: var(--strip-w); }}
|
||
/* Table wrapper trick: thead/tfoot repeat on every printed page */
|
||
.print-wrapper {{ width: 100%; }}
|
||
.print-wrapper > thead {{ display: table-header-group; }}
|
||
.print-wrapper > tfoot {{ display: table-footer-group; }}
|
||
.print-header-space {{ height: 16mm; display: block; }}
|
||
.print-footer-space {{ height: 16mm; display: block; }}
|
||
}}
|
||
html,body {{ -webkit-print-color-adjust:exact; print-color-adjust:exact; }}
|
||
:root {{
|
||
--text:#1a1a1a; --muted:#64748b; --line:#d1d5db; --soft:#f8fafc;
|
||
--oracle-red:#c74634; --header-green:#4a7c59; --row-alt:#eef3f0;
|
||
--fail:#c00000; --pass:#0f7b0f; --bad:#ef4444; --warn:#f59e0b; --ok:#22c55e;
|
||
--strip-w:28px;
|
||
}}
|
||
body {{ margin:0; font-family:Arial,Helvetica,'Liberation Sans',sans-serif; color:var(--text); background:#fff; font-size:11.5px; line-height:1.6; text-align:justify; }}
|
||
|
||
/* Decorative left strip on every page */
|
||
.page-strip {{ position:fixed; top:0; left:0; width:var(--strip-w); height:100%; z-index:100;
|
||
background: linear-gradient(180deg, #6b7f3a 0%, #8a9a4e 8%, #4a6741 16%, #2d3b2a 24%, #c4a94d 32%, #8b6d3b 40%, #3d5c3a 48%, #6b7f3a 56%, #c4a94d 64%, #4a6741 72%, #2d3b2a 80%, #8a9a4e 88%, #6b7f3a 100%);
|
||
}}
|
||
@media print {{
|
||
.page-strip {{ position:fixed; top:0; left:0; width:var(--strip-w); height:100vh; }}
|
||
}}
|
||
.wrap {{ margin-left:calc(var(--strip-w) + 10px); padding:0 28px 20px 20px; max-width:720px; overflow-x:hidden; }}
|
||
table {{ max-width:100%; overflow-wrap:break-word; }}
|
||
|
||
/* Oracle brand on every page (via running header simulation) */
|
||
.oracle-brand {{ font-family:Arial,Helvetica,'Liberation Sans',sans-serif; font-weight:700; letter-spacing:.8px; color:var(--oracle-red); font-size:15px; padding:18px 0 12px; }}
|
||
|
||
a {{ color:#2f5597; text-decoration:none; }}
|
||
a:hover {{ text-decoration:underline; }}
|
||
.page-break-before {{ break-before:page; page-break-before:always; }}
|
||
|
||
/* Cover */
|
||
.cover {{ position:relative; min-height:calc(297mm - 40mm); page-break-after:always; display:flex; flex-direction:column; }}
|
||
.cover-oracle {{ font-weight:700; letter-spacing:.8px; color:var(--oracle-red); font-size:15px; padding-top:18px; }}
|
||
.cover-rect {{ display:none; }}
|
||
.cover-spacer {{ flex:1; min-height:120px; }}
|
||
.cover-title {{ font-size:38px; line-height:1.12; font-weight:400; color:#111827; max-width:500px; margin-bottom:12px; text-align:left; }}
|
||
.cover-subtitle {{ font-size:12.5px; color:#374151; margin-bottom:6px; font-style:italic; }}
|
||
.cover-meta {{ font-size:10.5px; color:#374151; line-height:1.6; margin-top:8px; }}
|
||
.cover-bottom {{ min-height:80px; }}
|
||
|
||
/* Section page header */
|
||
.section-header-brand {{ font-weight:700; letter-spacing:.8px; color:var(--oracle-red); font-size:15px; padding:18px 0 14px; }}
|
||
|
||
/* Text blocks */
|
||
.text-block {{ margin:0 0 20px; }}
|
||
.text-block h2 {{ font-size:18px; font-weight:400; margin:0 0 10px; color:var(--oracle-red); }}
|
||
.text-block p {{ margin:0 0 10px; font-size:11px; line-height:1.65; overflow-wrap:anywhere; text-align:justify; color:#374151; }}
|
||
|
||
/* TOC */
|
||
.toc {{ margin:0 0 20px; }}
|
||
.toc-heading {{ color:var(--oracle-red); font-size:18px; font-weight:400; margin:0 0 10px; }}
|
||
.toc-rule {{ height:2px; background:var(--oracle-red); margin:0 0 14px; max-width:400px; }}
|
||
.toc-lines {{ display:flex; flex-direction:column; gap:3px; font-size:11px; }}
|
||
.toc-line {{ display:flex; align-items:baseline; gap:6px; }}
|
||
.toc-l0 {{ font-weight:700; padding:2px 0; }}
|
||
.toc-l1 {{ padding-left:20px; font-weight:400; color:#374151; }}
|
||
.toc-line a {{ flex:1; min-width:0; overflow-wrap:anywhere; }}
|
||
.toc-count {{ font-weight:700; color:var(--muted); min-width:20px; text-align:right; }}
|
||
|
||
/* Summary tables */
|
||
.summary-card {{ margin:0 0 20px; }}
|
||
.summary-card h1 {{ font-size:18px; font-weight:400; margin:0 0 4px; color:var(--oracle-red); }}
|
||
.summary-card .sub {{ font-size:11px; color:var(--muted); margin:0 0 12px; }}
|
||
table.domains {{ width:100%; border-collapse:collapse; border:1px solid #bbb; font-size:11px; table-layout:fixed; }}
|
||
table.domains thead {{ display:table-header-group; }}
|
||
table.domains thead th {{ background:var(--header-green); color:#fff; padding:8px 12px; text-transform:uppercase; letter-spacing:.4px; font-size:10px; text-align:center; border-right:1px solid rgba(255,255,255,.25); }}
|
||
table.domains thead th:first-child {{ text-align:left; }}
|
||
table.domains thead th:last-child {{ border-right:none; }}
|
||
table.domains tbody td {{ padding:7px 12px; border-top:1px solid #ccc; border-right:1px solid #ddd; }}
|
||
table.domains tbody tr {{ break-inside:avoid; page-break-inside:avoid; }}
|
||
table.domains tbody td:last-child {{ border-right:none; }}
|
||
table.domains tbody tr:nth-child(odd) {{ background:var(--row-alt); }}
|
||
.dom {{ font-weight:600; }}
|
||
.num {{ text-align:center; font-variant-numeric:tabular-nums; }}
|
||
.fail {{ color:var(--fail); font-weight:700; }}
|
||
.pass {{ color:var(--pass); font-weight:700; }}
|
||
.warn {{ color:#e67e22; font-weight:700; }}
|
||
.totals {{ font-size:12px; margin-top:10px; color:var(--muted); }}
|
||
|
||
/* Benchmark summary table */
|
||
table.benchmark {{ width:100%; border-collapse:collapse; border:1px solid #bbb; font-size:11px; table-layout:fixed; }}
|
||
table.benchmark thead {{ display:table-header-group; }}
|
||
table.benchmark thead th {{ background:var(--header-green); color:#fff; padding:8px 12px; text-align:left; font-size:10px; text-transform:uppercase; border-right:1px solid rgba(255,255,255,.25); }}
|
||
table.benchmark thead th:last-child {{ text-align:center; border-right:none; }}
|
||
table.benchmark tbody td {{ padding:6px 12px; border-top:1px solid #ddd; }}
|
||
table.benchmark tbody tr {{ break-inside:avoid; page-break-inside:avoid; }}
|
||
table.benchmark .section-header {{ break-after:avoid; page-break-after:avoid; }}
|
||
table.benchmark .section-header td {{ background:#e8ece8; font-weight:700; font-size:11px; padding:8px 12px; border-top:2px solid #aaa; }}
|
||
.rec-num {{ width:42px; font-weight:600; color:#2f5597; white-space:nowrap; }}
|
||
.pass-text {{ color:var(--pass); font-weight:700; }}
|
||
.fail-text {{ color:var(--fail); font-weight:700; }}
|
||
|
||
/* Detail sections */
|
||
.detail-section {{ margin-top:0; }}
|
||
.detail-section h1.section-num {{ font-size:18px; color:var(--oracle-red); font-weight:400; border-bottom:none; padding-bottom:4px; margin:0 0 16px; }}
|
||
.finding {{ border-bottom:1px solid var(--line); padding:14px 0; margin:0; }}
|
||
.finding:last-child {{ border-bottom:none; }}
|
||
.finding h2 {{ font-size:12.5px; font-weight:700; margin:0 0 8px; color:#111827; line-height:1.4; text-align:left; }}
|
||
h1, h2, h3, h4 {{ text-align:left; }}
|
||
.oracle-brand {{ text-align:left; }}
|
||
|
||
/* Result box */
|
||
.result-box {{ padding:8px 12px; border-radius:6px; font-size:11px; margin:8px 0; }}
|
||
.result-box.ok {{ background:#f0fdf4; border-left:3px solid var(--pass); }}
|
||
.result-box.fail {{ background:#fef2f2; border-left:3px solid var(--fail); }}
|
||
|
||
/* Findings file link */
|
||
.findings-file {{ display:flex; align-items:center; gap:8px; margin:8px 0; padding:8px 12px; background:#fffbeb; border-left:3px solid #f59e0b; border-radius:4px; }}
|
||
.file-link {{ font-size:11.5px; font-weight:700; color:#2f5597; text-decoration:none; }}
|
||
.file-link:hover {{ text-decoration:underline; }}
|
||
.file-hint {{ font-size:10px; color:var(--muted); }}
|
||
@media print {{ .file-link {{ color:#2f5597 !important; }} }}
|
||
|
||
/* Description */
|
||
.finding-desc {{ margin:10px 0 0; }}
|
||
.finding-desc h4 {{ font-size:11.5px; margin:0 0 6px; color:#2f5597; font-weight:700; }}
|
||
.finding-desc p {{ font-size:11px; line-height:1.65; text-align:justify; color:#374151; margin:0; padding:10px 14px; background:#f8f9fa; border-left:3px solid #2f5597; }}
|
||
|
||
/* Rationale */
|
||
.finding-rationale {{ margin:10px 0 0; }}
|
||
.finding-rationale h4 {{ font-size:11.5px; margin:0 0 6px; color:#92400e; font-weight:700; }}
|
||
.finding-rationale p {{ font-size:11px; line-height:1.65; text-align:justify; color:#374151; margin:0; padding:10px 14px; background:#fffbeb; border-left:3px solid #f59e0b; }}
|
||
|
||
/* Remediation */
|
||
.remediation {{ margin:10px 0 0; }}
|
||
.remediation h4 {{ font-size:11.5px; margin:0 0 6px; color:var(--oracle-red); font-weight:700; }}
|
||
.remediation-content {{ font-size:11px; line-height:1.6; }}
|
||
.rem-step {{ display:flex; gap:6px; padding:3px 0; align-items:flex-start; }}
|
||
.rem-step .step-num {{ font-weight:700; color:#2f5597; min-width:20px; }}
|
||
.rem-text {{ margin:4px 0; text-align:justify; color:#374151; }}
|
||
.code-snippet {{ background:#1e293b; color:#e2e8f0; padding:10px 14px; border-radius:4px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:10.5px; line-height:1.5; overflow-x:auto; white-space:pre-wrap; word-break:break-all; margin:6px 0; border-left:3px solid #2f5597; }}
|
||
|
||
/* Compliance bar */
|
||
.compliance-bar {{ display:flex; align-items:center; gap:10px; margin:6px 0 8px; }}
|
||
.bar-label {{ font-size:11px; font-weight:700; width:100px; white-space:nowrap; }}
|
||
.bar {{ flex:1; height:8px; background:#eef2ff; border:1px solid #e5e7eb; border-radius:999px; overflow:hidden; }}
|
||
.fill {{ height:100%; border-radius:999px; }}
|
||
.fill.bad {{ background:var(--bad); }}
|
||
.fill.warn {{ background:var(--warn); }}
|
||
.fill.ok {{ background:var(--ok); }}
|
||
|
||
/* Level badges */
|
||
.level-badge {{ display:inline-block; padding:1px 8px; border-radius:10px; font-size:9px; font-weight:700; vertical-align:middle; margin-left:6px; }}
|
||
.level-1 {{ background:#e8f5e9; color:#2e7d32; border:1px solid #a5d6a7; }}
|
||
.level-2 {{ background:#fff3e0; color:#e65100; border:1px solid #ffcc80; }}
|
||
|
||
/* Finding references (CIS v8, CCCS) */
|
||
.finding-refs {{ font-size:9px; color:var(--muted); margin:-4px 0 6px; }}
|
||
|
||
/* Text block formatting */
|
||
.finding-desc p, .finding-rationale p {{ word-wrap:break-word; text-align:justify; }}
|
||
.remediation-content {{ word-wrap:break-word; text-align:justify; }}
|
||
|
||
/* Findings resource table */
|
||
.findings-table-wrap {{ margin:10px 0 0; overflow-x:auto; }}
|
||
.findings-table {{ width:100%; border-collapse:collapse; font-size:9px; border:1px solid #ccc; table-layout:fixed; }}
|
||
.findings-table thead th {{ background:#4a6741; color:#fff; padding:3px 5px; text-align:left; font-size:8px; text-transform:uppercase; letter-spacing:.2px; word-wrap:break-word; overflow:hidden; }}
|
||
.findings-table tbody td {{ padding:2px 5px; border-top:1px solid #e5e5e5; color:#374151; word-wrap:break-word; overflow-wrap:break-word; overflow:hidden; font-size:8px; }}
|
||
.findings-table tbody tr:nth-child(even) {{ background:#f4f6f4; }}
|
||
.findings-note {{ font-size:9px; color:var(--muted); margin:4px 0 0; font-style:italic; }}
|
||
|
||
/* Print button */
|
||
.print-btn {{ position:fixed; bottom:20px; right:20px; padding:12px 24px; background:var(--oracle-red); color:#fff; border:none; border-radius:6px; font-size:14px; font-weight:700; cursor:pointer; box-shadow:0 4px 12px rgba(0,0,0,.25); z-index:999; }}
|
||
.print-btn:hover {{ background:#a83a2c; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<!-- Decorative left strip -->
|
||
<div class="page-strip"></div>
|
||
|
||
<table class="print-wrapper"><thead><tr><td><div class="print-header-space"></div></td></tr></thead>
|
||
<tfoot><tr><td><div class="print-footer-space"></div></td></tr></tfoot>
|
||
<tbody><tr><td>
|
||
<div class="wrap">
|
||
|
||
<!-- COVER -->
|
||
<div class="cover">
|
||
<div class="cover-oracle">ORACLE</div>
|
||
<div class="cover-spacer"></div>
|
||
<div class="cover-title">CIS Foundations Benchmark Assessment</div>
|
||
<div class="cover-subtitle">{esc(tenancy_name)} – Oracle Cloud Infrastructure</div>
|
||
<div class="cover-meta">
|
||
<div><b>Tenancy:</b> {esc(tenancy_name)}</div>
|
||
<div>{esc(month_year)}, Version [1.0]</div>
|
||
{f'<div><b>Extract date:</b> {esc(extract_date)}</div>' if extract_date else ''}
|
||
<div>Copyright © {now.year}, Oracle and/or its affiliates</div>
|
||
<div>Confidential – Oracle Restricted</div>
|
||
</div>
|
||
<div class="cover-bottom"></div>
|
||
</div>
|
||
|
||
<!-- DISCLAIMER + PROFILE DEFINITIONS -->
|
||
<div class="oracle-brand">ORACLE</div>
|
||
<div class="text-block">
|
||
<h2>Disclaimer</h2>
|
||
<p>{esc(DISCLAIMER_TEXT)}</p>
|
||
</div>
|
||
<div class="text-block">
|
||
<h2>Profile Definitions</h2>
|
||
<p>{PROFILE_DEFINITIONS}</p>
|
||
</div>
|
||
|
||
<!-- TOC -->
|
||
<div class="page-break-before">
|
||
<div class="oracle-brand">ORACLE</div>
|
||
<div class="toc">
|
||
<div class="toc-heading">Table of contents</div>
|
||
<div class="toc-rule"></div>
|
||
<div class="toc-lines">{toc_lines}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SECURITY OVERVIEW -->
|
||
<div class="page-break-before">
|
||
<div class="oracle-brand">ORACLE</div>
|
||
<div class="text-block">
|
||
<h2>Security Overview</h2>
|
||
<p>{SECURITY_OVERVIEW}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CIS SECURITY ASSESSMENT SUMMARY -->
|
||
<div class="page-break-before" id="cis-summary">
|
||
<div class="oracle-brand">ORACLE</div>
|
||
<div class="summary-card">
|
||
<h1>CIS Security Assessment</h1>
|
||
<div class="sub">CIS Oracle Cloud Infrastructure Foundations Benchmark v3.0.0</div>
|
||
<p style="margin-bottom:12px">Number of controls per domain and non-compliant controls on tenancy <b>{esc(tenancy_name)}</b>:</p>
|
||
<table class="domains">
|
||
<thead><tr><th style="text-align:left">DOMAINS</th><th>TOTAL CONTROLS</th><th>FAILED</th><th>PASSED</th></tr></thead>
|
||
<tbody>{domains_rows}</tbody>
|
||
</table>
|
||
<div class="totals">Total: <b>{total_all}</b> • Passed: <b class="pass">{total_passed}</b> • Failed: <b class="fail">{total_failed}</b> • Compliance: <b>{pct_global}%</b></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CIS BENCHMARK RECOMMENDATION SUMMARY -->
|
||
<div class="page-break-before" id="benchmark-table">
|
||
<div class="oracle-brand">ORACLE</div>
|
||
<div class="summary-card">
|
||
<h1>CIS Benchmark Recommendation Summary</h1>
|
||
<div class="sub">Status per recommendation</div>
|
||
<table class="benchmark">
|
||
<thead><tr><th style="width:50px">Item</th><th>Description</th><th style="width:40px;text-align:center">Level</th><th style="width:70px;text-align:center">Status</th></tr></thead>
|
||
<tbody>{summary_rows}</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DETAILED SECTIONS -->
|
||
{sections_html}
|
||
|
||
<!-- OCI SERVICES -->
|
||
{oci_services_html}
|
||
|
||
<!-- BACK PAGE — Connect with us -->
|
||
<div class="page-break-before" id="back-page">
|
||
<div class="oracle-brand">ORACLE</div>
|
||
<div class="text-block" style="margin-top:30px">
|
||
<h2 style="font-family:serif;font-weight:700;font-size:16px;margin-bottom:12px">Connect with us</h2>
|
||
<p>Call +<b>1.800.ORACLE1</b> or visit <b>oracle.com</b>. Outside North America, find your local office at: <b>oracle.com/contact</b>.</p>
|
||
<div style="display:flex;gap:40px;margin:16px 0 28px 0;font-size:11px;color:var(--body-text,#333);align-items:center">
|
||
<span style="display:inline-flex;align-items:center;gap:6px"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg> blogs.oracle.com</span>
|
||
<span style="display:inline-flex;align-items:center;gap:6px"><svg width="14" height="14" viewBox="0 0 24 24" fill="#1877f2"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"/></svg> facebook.com/oracle</span>
|
||
<span style="display:inline-flex;align-items:center;gap:6px"><svg width="14" height="14" viewBox="0 0 24 24" fill="#333"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg> twitter.com/oracle</span>
|
||
</div>
|
||
<p style="font-size:8px;color:#666;text-align:justify;line-height:1.5">
|
||
Copyright © {now.year}, Oracle and/or its affiliates. This document is provided for information purposes only, and the contents hereof are subject to change without notice. This document is not warranted to be error-free, nor subject to any other warranties or conditions, whether expressed orally or implied in law, including implied warranties and conditions of merchantability or fitness for a particular purpose. We specifically disclaim any liability with respect to this document, and no contractual obligations are formed either directly or indirectly by this document. This document may not be reproduced or transmitted in any form or by any means, electronic or mechanical, for any purpose, without our prior written permission.
|
||
</p>
|
||
<p style="font-size:8px;color:#666;margin-top:8px">
|
||
Oracle, Java, MySQL, and NetSuite are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</td></tr></tbody></table>
|
||
|
||
<!-- Print/PDF button -->
|
||
<button class="print-btn no-print" onclick="window.print()">🖶 Download PDF</button>
|
||
<script>
|
||
// Add auth token to findings file download links
|
||
document.addEventListener('DOMContentLoaded', function() {{
|
||
var token = '';
|
||
try {{ token = new URLSearchParams(window.location.search).get('token') || ''; }} catch(e) {{}}
|
||
if (!token) try {{ token = window.parent.localStorage.getItem('t') || ''; }} catch(e) {{}}
|
||
if (!token) try {{ token = localStorage.getItem('t') || ''; }} catch(e) {{}}
|
||
document.querySelectorAll('.file-link').forEach(function(a) {{
|
||
if (token && a.href.includes('/download')) {{
|
||
a.href += (a.href.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(token);
|
||
}}
|
||
}});
|
||
}});
|
||
</script>
|
||
|
||
</body>
|
||
</html>'''
|
||
|
||
|
||
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
|
||
|
||
|