feat: compliance report v2 — title, disclaimer, level badges, resource tables

- Title: "Oracle Cloud Infrastructure (OCI) CIS Foundations Benchmark Assessment"
- Purpose Statement removed, replaced by single Disclaimer (benchmark standard + legal protection)
- Profile Definitions section added (Level 1 and Level 2 descriptions from CIS page 13)
- Level badge on each finding (green Level 1, orange Level 2)
- Level column in benchmark summary table
- Formatting: pre-wrap + line breaks in description/rationale for long policy text
- Resource tables: up to 7 columns (was 4), shows all useful columns per finding type
- Age calculation: time_created/time_expires shown as "2025-01-16 (434d)"
- OCID display: truncated with start+end visible "ocid1...3ikc3ma"
- Column labels: user-friendly names (User, Key ID, Created (Age), etc.)
- Removed id from skip list, added user_name/fingerprint/time_created to priority
This commit is contained in:
nogueiraguh
2026-03-26 19:40:09 -03:00
parent 21205cb093
commit 937d0c8318

View File

@@ -5084,55 +5084,76 @@ def _build_findings_table(csv_path: str, max_rows: int = 10) -> str:
def esc(v): return (v or "").replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
def fmt(v):
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[-12:]
if len(v) > 50:
return v[:47] + "..."
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
# Priority columns: pick the most useful ones, max 4 columns
priority = ["name", "display_name", "username", "lifecycle_state", "status",
"is_mfa_activated", "domain_name", "description", "email",
"key_id", "age", "region", "cidr_block", "protocol", "port_range",
"visibility", "encryption", "versioning", "type", "severity"]
# 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", "compartment_id",
"domain_id", "external_identifier", "freeform_tags", "system_tags", "id",
"time_created", "email_verified", "is_federated", "groups",
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",
"last_successful_login_date"}
avail = [c for c in all_cols if c.lower() not in skip]
# Pick up to 4 columns in priority order
display = []
for p_col in priority:
for a_col in avail:
if a_col.lower() == p_col and a_col not in display:
display.append(a_col)
break
if len(display) >= 4:
break
if not display:
display = avail[:4]
"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 c.replace("_", " ").title()
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, "")))}</td>' for c in display)
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 ""}.'
@@ -5175,13 +5196,15 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
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="3"><b>{esc(sec)}</b></td></tr>'
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"
summary_rows += f'<tr><td class="rec-num">{esc(rec)}</td><td>{esc(title)}</td><td class="num {status_cls}">{status_txt}</td></tr>'
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 = ""
@@ -5194,6 +5217,7 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
for r in sd["items"]:
rec = r.get("Recommendation #", "")
title = r.get("Title", "")
level = (r.get("Level", "") or "").strip()
rec_id = f"rec-{slug(sec_name)}-{slug(rec)}"
is_compliant = r["__compliant"]
findings = r.get("Findings", "").strip()
@@ -5218,15 +5242,17 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
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>'
# Description
# Description — preserve line breaks for readability
description_html = ""
if not is_compliant and rag_desc:
description_html = f'<div class="finding-desc"><h4>Description:</h4><p>{esc(rag_desc)}</p></div>'
desc_formatted = esc(rag_desc).replace('\n', '<br/>')
description_html = f'<div class="finding-desc"><h4>Description:</h4><p>{desc_formatted}</p></div>'
# Rationale (why this matters)
# Rationale — preserve line breaks
rationale_html = ""
if not is_compliant and rag_rationale:
rationale_html = f'<div class="finding-rationale"><h4>Rationale:</h4><p>{esc(rag_rationale)}</p></div>'
rat_formatted = esc(rag_rationale).replace('\n', '<br/>')
rationale_html = f'<div class="finding-rationale"><h4>Rationale:</h4><p>{rat_formatted}</p></div>'
# Remediation — formatted with code snippets
remediation_html = ""
@@ -5257,9 +5283,14 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
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>'
sections_html += f'''
<div class="finding" id="{esc(rec_id)}">
<h2>{esc(rec)} {esc(title)}</h2>
<h2>{esc(rec)} {esc(title)} {level_badge}</h2>
{bar_html}
{result_html}
{findings_table_html}
@@ -5283,8 +5314,11 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
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>'
PURPOSE_TEXT = "The purpose of the cloud security assessment is to comprehensively evaluate the organization's security posture in its cloud environment, identifying vulnerabilities and gaps that could compromise data integrity, confidentiality, and availability. This assessment aims to enhance information security maturity, strengthen defenses against cyber threats, ensure compliance with regulations, and foster a culture of security awareness. Through periodic assessments, the organization proactively mitigates risks, ensuring continuous security for cloud-configured assets and maintaining stakeholders' trust in data protection capabilities."
DISCLAIMER_TEXT = "This document in any form, software or printed matter, contains proprietary information that is the exclusive property of Oracle. Your access to and use of this confidential material is subject to the terms and conditions of your Oracle software license and service agreement, which has been executed and with which you agree to comply. This document and information contained herein may not be disclosed, copied, reproduced or distributed to anyone outside Oracle without prior written consent of Oracle. This document is not part of your license agreement, nor can it be incorporated into any contractual agreement with Oracle or its subsidiaries or affiliates.\n\nThis document is for informational purposes only and is intended solely to assist you in planning for the implementation and upgrade of the product features described. It is not a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions."
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.
@@ -5305,7 +5339,7 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>LAD A-Team CIS Compliance Report — {esc(tenancy_name)}</title>
<title>OCI CIS Foundations Benchmark Assessment — {esc(tenancy_name)}</title>
<style>
@page {{ size: A4; margin: 0; }}
@media screen {{
@@ -5457,6 +5491,14 @@ table.benchmark .section-header td {{ background:#e8ece8; font-weight:700; font-
.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; }}
/* Text block formatting — ensure line breaks in long content */
.finding-desc p, .finding-rationale p, .remediation-content {{ white-space:pre-wrap; word-wrap:break-word; }}
/* Findings resource table */
.findings-table-wrap {{ margin:10px 0 0; }}
.findings-table {{ width:100%; border-collapse:collapse; font-size:9.5px; border:1px solid #ccc; }}
@@ -5483,7 +5525,7 @@ table.benchmark .section-header td {{ background:#e8ece8; font-weight:700; font-
<div class="cover">
<div class="cover-oracle">ORACLE</div>
<div class="cover-spacer"></div>
<div class="cover-title">Oracle Cloud Security<br/>Assessment</div>
<div class="cover-title">Oracle Cloud Infrastructure (OCI)<br/>CIS Foundations Benchmark<br/>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>
@@ -5495,16 +5537,16 @@ table.benchmark .section-header td {{ background:#e8ece8; font-weight:700; font-
<div class="cover-bottom"></div>
</div>
<!-- PURPOSE + DISCLAIMER -->
<!-- DISCLAIMER + PROFILE DEFINITIONS -->
<div class="oracle-brand">ORACLE</div>
<div class="text-block">
<h2>Purpose Statement</h2>
<p>{esc(PURPOSE_TEXT)}</p>
</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">
@@ -5547,7 +5589,7 @@ table.benchmark .section-header td {{ background:#e8ece8; font-weight:700; font-
<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:70px;text-align:center">Status</th></tr></thead>
<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>