fix: compliance report Oracle design, polling robustness, print margins
- Oracle PDF style: decorative left strip, ORACLE brand header, red section titles, green table headers - Cover page: removed rectangle, clean layout matching Oracle reference PDF - Print: @page margin:0 + thead/tfoot table trick for per-page header/footer spacing without browser URL/date - RAG remediation: fixed fallback dumping entire CIS chunk instead of just remediation section - Findings: removed break-inside:avoid that caused empty pages, emoji replaced with [CSV] text - Tables: break-inside:avoid on rows, section headers break-after:avoid, thead repeats on page break - Compliance polling: useEffect-based with cleanup, auto-expand on ready, re-check on section toggle - Explorer: silent polling during start/stop (no flickering) - HTML report starts minimized, auto-expands on fresh generation - Include dist/ in git for deployment
This commit is contained in:
183
backend/app.py
183
backend/app.py
@@ -4272,7 +4272,7 @@ def _search_cis_remediation(title: str, rec_num: str, user_id: str) -> dict:
|
|||||||
# Try second chunk if first has no remediation
|
# Try second chunk if first has no remediation
|
||||||
if not rem and len(matched) > 1:
|
if not rem and len(matched) > 1:
|
||||||
rem = _extract_remediation_section(matched[1])
|
rem = _extract_remediation_section(matched[1])
|
||||||
return {"description": desc, "remediation": rem or best}
|
return {"description": desc, "remediation": rem}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"CIS remediation search failed for {adb_cfg.get('config_name','?')}: {e}")
|
log.warning(f"CIS remediation search failed for {adb_cfg.get('config_name','?')}: {e}")
|
||||||
return {"description": "", "remediation": ""}
|
return {"description": "", "remediation": ""}
|
||||||
@@ -4323,6 +4323,7 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
|
|||||||
for sec_name, sd in sections_data.items():
|
for sec_name, sd in sections_data.items():
|
||||||
sec_id = f"section-{slug(sec_name)}"
|
sec_id = f"section-{slug(sec_name)}"
|
||||||
sections_html += f'<div class="detail-section page-break-before" id="{esc(sec_id)}">'
|
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>'
|
sections_html += f'<h1 class="section-num">{esc(sec_name)}</h1>'
|
||||||
|
|
||||||
for r in sd["items"]:
|
for r in sd["items"]:
|
||||||
@@ -4377,7 +4378,7 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
|
|||||||
fid = file_id_map.get(csv_filename, "")
|
fid = file_id_map.get(csv_filename, "")
|
||||||
if fid and rid:
|
if fid and rid:
|
||||||
dl_url = f"/api/reports/{rid}/files/{fid}/download"
|
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">📄 {esc(csv_filename)}</a><span class="file-hint">Affected resources details</span></div>'
|
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>'
|
||||||
|
|
||||||
sections_html += f'''
|
sections_html += f'''
|
||||||
<div class="finding" id="{esc(rec_id)}">
|
<div class="finding" id="{esc(rec_id)}">
|
||||||
@@ -4427,125 +4428,169 @@ def _build_compliance_html(filtered_rows: list, tenancy_name: str, extract_date:
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<title>LAD A-Team CIS Compliance Report — {esc(tenancy_name)}</title>
|
<title>LAD A-Team CIS Compliance Report — {esc(tenancy_name)}</title>
|
||||||
<style>
|
<style>
|
||||||
@page {{ size: A4; margin: 14mm; }}
|
@page {{ size: A4; margin: 0; }}
|
||||||
@media print {{ .no-print {{ display: none !important; }} body {{ background: #fff !important; }} }}
|
@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; }}
|
html,body {{ -webkit-print-color-adjust:exact; print-color-adjust:exact; }}
|
||||||
:root {{
|
:root {{
|
||||||
--text:#0f172a; --muted:#64748b; --line:#e2e8f0; --soft:#f8fafc;
|
--text:#1a1a1a; --muted:#64748b; --line:#d1d5db; --soft:#f8fafc;
|
||||||
--oracle-red:#ff0000; --header-blue:#2f5597; --row-alt:#e8f0f7;
|
--oracle-red:#c74634; --header-green:#4a7c59; --row-alt:#eef3f0;
|
||||||
--fail:#c00000; --pass:#0f7b0f; --bad:#ef4444; --warn:#f59e0b; --ok:#22c55e;
|
--fail:#c00000; --pass:#0f7b0f; --bad:#ef4444; --warn:#f59e0b; --ok:#22c55e;
|
||||||
|
--strip-w:28px;
|
||||||
}}
|
}}
|
||||||
body {{ margin:0; font-family:Arial,Helvetica,sans-serif; color:var(--text); background:#fff; font-size:12px; line-height:1.5; }}
|
body {{ margin:0; font-family:'Oracle Sans','Segoe UI',Arial,Helvetica,sans-serif; color:var(--text); background:#fff; font-size:11.5px; line-height:1.6; }}
|
||||||
.wrap {{ max-width:960px; margin:0 auto; padding:0 14px; }}
|
|
||||||
a {{ color:var(--header-blue); text-decoration:none; }}
|
/* 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; }}
|
||||||
|
|
||||||
|
/* Oracle brand on every page (via running header simulation) */
|
||||||
|
.oracle-brand {{ font-family:'Oracle Sans',Arial,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; }}
|
a:hover {{ text-decoration:underline; }}
|
||||||
.page-break-before {{ break-before:page; page-break-before:always; }}
|
.page-break-before {{ break-before:page; page-break-before:always; }}
|
||||||
|
|
||||||
/* Cover */
|
/* Cover */
|
||||||
.cover {{ position:relative; min-height:calc(297mm - 28mm); page-break-after:always; }}
|
.cover {{ position:relative; min-height:calc(297mm - 40mm); page-break-after:always; display:flex; flex-direction:column; }}
|
||||||
.cover-oracle {{ position:absolute; top:0; left:0; font-weight:700; letter-spacing:.6px; color:var(--oracle-red); font-size:18px; }}
|
.cover-oracle {{ font-weight:700; letter-spacing:.8px; color:var(--oracle-red); font-size:15px; padding-top:18px; }}
|
||||||
.cover-rect {{ position:absolute; top:26mm; left:6mm; right:6mm; height:82mm; border:1px solid #9ca3af; }}
|
.cover-rect {{ display:none; }}
|
||||||
.cover-title {{ position:absolute; left:0; bottom:62mm; font-size:42px; line-height:1.08; font-weight:500; color:#111827; max-width:165mm; }}
|
.cover-spacer {{ flex:1; min-height:120px; }}
|
||||||
.cover-subtitle {{ position:absolute; left:0; bottom:52mm; font-size:12px; color:#111827; }}
|
.cover-title {{ font-size:38px; line-height:1.12; font-weight:400; color:#111827; max-width:500px; margin-bottom:12px; }}
|
||||||
.cover-meta {{ position:absolute; left:0; bottom:18mm; font-size:11px; color:#111827; line-height:1.45; }}
|
.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 blocks */
|
||||||
.text-block {{ border:1px solid var(--line); border-radius:10px; padding:14px 18px; margin:0 0 14px; break-inside:avoid; page-break-inside:avoid; }}
|
.text-block {{ margin:0 0 20px; }}
|
||||||
.text-block h2 {{ font-size:14px; margin:0 0 8px; color:var(--oracle-red); }}
|
.text-block h2 {{ font-size:18px; font-weight:400; margin:0 0 10px; color:var(--oracle-red); }}
|
||||||
.text-block p {{ margin:0; font-size:11px; line-height:1.5; white-space:pre-wrap; overflow-wrap:anywhere; text-align:justify; }}
|
.text-block p {{ margin:0 0 10px; font-size:11px; line-height:1.65; white-space:pre-wrap; overflow-wrap:anywhere; text-align:justify; color:#374151; }}
|
||||||
|
|
||||||
/* TOC */
|
/* TOC */
|
||||||
.toc {{ margin:0 0 14px; }}
|
.toc {{ margin:0 0 20px; }}
|
||||||
.toc-brand {{ color:var(--oracle-red); font-weight:700; letter-spacing:.6px; font-size:16px; margin:0 0 6px; }}
|
.toc-heading {{ color:var(--oracle-red); font-size:18px; font-weight:400; margin:0 0 10px; }}
|
||||||
.toc-heading {{ color:var(--oracle-red); font-size:14px; margin:0 0 10px; }}
|
.toc-rule {{ height:2px; background:var(--oracle-red); margin:0 0 14px; max-width:400px; }}
|
||||||
.toc-rule {{ height:1px; background:#cbd5e1; margin:0 0 10px; }}
|
.toc-lines {{ display:flex; flex-direction:column; gap:3px; font-size:11px; }}
|
||||||
.toc-lines {{ display:flex; flex-direction:column; gap:5px; font-size:11px; }}
|
|
||||||
.toc-line {{ display:flex; align-items:baseline; gap:6px; }}
|
.toc-line {{ display:flex; align-items:baseline; gap:6px; }}
|
||||||
.toc-l0 {{ font-weight:700; }}
|
.toc-l0 {{ font-weight:700; padding:2px 0; }}
|
||||||
.toc-l1 {{ padding-left:16px; font-weight:400; color:#374151; }}
|
.toc-l1 {{ padding-left:20px; font-weight:400; color:#374151; }}
|
||||||
.toc-line a {{ flex:1; min-width:0; overflow-wrap:anywhere; }}
|
.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; }}
|
.toc-count {{ font-weight:700; color:var(--muted); min-width:20px; text-align:right; }}
|
||||||
|
|
||||||
/* Summary tables */
|
/* Summary tables */
|
||||||
.summary-card {{ border:1px solid var(--line); border-radius:10px; padding:14px 18px; margin:0 0 14px; }}
|
.summary-card {{ margin:0 0 20px; }}
|
||||||
.summary-card h1 {{ font-size:16px; margin:0 0 4px; }}
|
.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 10px; }}
|
.summary-card .sub {{ font-size:11px; color:var(--muted); margin:0 0 12px; }}
|
||||||
table.domains {{ width:100%; border-collapse:collapse; border:1px solid #cbd5e1; font-size:11px; }}
|
table.domains {{ width:100%; border-collapse:collapse; border:1px solid #bbb; font-size:11px; }}
|
||||||
table.domains thead th {{ background:var(--header-blue); color:#fff; padding:7px 10px; text-transform:uppercase; letter-spacing:.4px; font-size:10px; text-align:center; border-right:1px solid rgba(255,255,255,.2); }}
|
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:first-child {{ text-align:left; }}
|
||||||
table.domains thead th:last-child {{ border-right:none; }}
|
table.domains thead th:last-child {{ border-right:none; }}
|
||||||
table.domains tbody td {{ padding:6px 10px; border-top:1px solid #cbd5e1; border-right:1px solid #cbd5e1; }}
|
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 td:last-child {{ border-right:none; }}
|
||||||
table.domains tbody tr:nth-child(odd) {{ background:var(--row-alt); }}
|
table.domains tbody tr:nth-child(odd) {{ background:var(--row-alt); }}
|
||||||
.dom {{ font-weight:600; }}
|
.dom {{ font-weight:600; }}
|
||||||
.num {{ text-align:center; font-variant-numeric:tabular-nums; }}
|
.num {{ text-align:center; font-variant-numeric:tabular-nums; }}
|
||||||
.fail {{ color:var(--fail); font-weight:700; }}
|
.fail {{ color:var(--fail); font-weight:700; }}
|
||||||
.pass {{ color:var(--pass); font-weight:700; }}
|
.pass {{ color:var(--pass); font-weight:700; }}
|
||||||
.totals {{ font-size:12px; margin-top:8px; color:var(--muted); }}
|
.totals {{ font-size:12px; margin-top:10px; color:var(--muted); }}
|
||||||
|
|
||||||
/* Benchmark summary table */
|
/* Benchmark summary table */
|
||||||
table.benchmark {{ width:100%; border-collapse:collapse; border:1px solid #cbd5e1; font-size:11px; }}
|
table.benchmark {{ width:100%; border-collapse:collapse; border:1px solid #bbb; font-size:11px; }}
|
||||||
table.benchmark thead th {{ background:var(--header-blue); color:#fff; padding:7px 10px; text-align:left; font-size:10px; text-transform:uppercase; border-right:1px solid rgba(255,255,255,.2); }}
|
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 thead th:last-child {{ text-align:center; border-right:none; }}
|
||||||
table.benchmark tbody td {{ padding:5px 10px; border-top:1px solid #e5e7eb; }}
|
table.benchmark tbody td {{ padding:6px 12px; border-top:1px solid #ddd; }}
|
||||||
table.benchmark .section-header td {{ background:#f1f5f9; font-weight:700; font-size:11px; padding:8px 10px; border-top:2px solid #cbd5e1; }}
|
table.benchmark tbody tr {{ break-inside:avoid; page-break-inside:avoid; }}
|
||||||
.rec-num {{ width:40px; font-weight:600; color:var(--header-blue); white-space:nowrap; }}
|
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; }}
|
.pass-text {{ color:var(--pass); font-weight:700; }}
|
||||||
.fail-text {{ color:var(--fail); font-weight:700; }}
|
.fail-text {{ color:var(--fail); font-weight:700; }}
|
||||||
|
|
||||||
/* Detail sections */
|
/* Detail sections */
|
||||||
.detail-section {{ margin-top:20px; }}
|
.detail-section {{ margin-top:0; }}
|
||||||
.detail-section h1.section-num {{ font-size:18px; color:var(--header-blue); border-bottom:2px solid var(--header-blue); padding-bottom:6px; margin:0 0 16px; }}
|
.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:1px solid var(--line); border-radius:10px; padding:14px 18px; margin:0 0 14px; break-inside:avoid; page-break-inside:avoid; }}
|
.finding {{ border-bottom:1px solid var(--line); padding:14px 0; margin:0; }}
|
||||||
.finding h2 {{ font-size:13px; font-weight:700; margin:0 0 8px; color:#111827; line-height:1.35; }}
|
.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; }}
|
||||||
|
|
||||||
/* Result box */
|
/* Result box */
|
||||||
.result-box {{ padding:8px 12px; border-radius:8px; font-size:11px; margin:8px 0; }}
|
.result-box {{ padding:8px 12px; border-radius:6px; font-size:11px; margin:8px 0; }}
|
||||||
.result-box.ok {{ background:#f0fdf4; border:1px solid #bbf7d0; }}
|
.result-box.ok {{ background:#f0fdf4; border-left:3px solid var(--pass); }}
|
||||||
.result-box.fail {{ background:#fef2f2; border:1px solid #fecaca; }}
|
.result-box.fail {{ background:#fef2f2; border-left:3px solid var(--fail); }}
|
||||||
|
|
||||||
/* Findings file link */
|
/* Findings file link */
|
||||||
.findings-file {{ display:flex; align-items:center; gap:8px; margin:8px 0; padding:8px 12px; background:#fffbeb; border:1px solid #fde68a; border-radius:8px; }}
|
.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:12px; font-weight:700; color:var(--header-blue); text-decoration:none; }}
|
.file-link {{ font-size:11.5px; font-weight:700; color:#2f5597; text-decoration:none; }}
|
||||||
.file-link:hover {{ text-decoration:underline; }}
|
.file-link:hover {{ text-decoration:underline; }}
|
||||||
.file-hint {{ font-size:10px; color:var(--muted); }}
|
.file-hint {{ font-size:10px; color:var(--muted); }}
|
||||||
@media print {{ .file-link {{ color:var(--header-blue) !important; }} }}
|
@media print {{ .file-link {{ color:#2f5597 !important; }} }}
|
||||||
|
|
||||||
/* Description */
|
/* Description */
|
||||||
.finding-desc {{ margin:10px 0 0; }}
|
.finding-desc {{ margin:10px 0 0; }}
|
||||||
.finding-desc h4 {{ font-size:12px; margin:0 0 6px; color:var(--header-blue); }}
|
.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.6; text-align:justify; color:#374151; margin:0; padding:10px 14px; background:#f0f4ff; border:1px solid #dbeafe; border-radius:8px; }}
|
.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; }}
|
||||||
|
|
||||||
/* Remediation */
|
/* Remediation */
|
||||||
.remediation {{ margin:10px 0 0; }}
|
.remediation {{ margin:10px 0 0; }}
|
||||||
.remediation h4 {{ font-size:12px; margin:0 0 6px; color:var(--oracle-red); }}
|
.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.55; }}
|
.remediation-content {{ font-size:11px; line-height:1.6; }}
|
||||||
.rem-step {{ display:flex; gap:6px; padding:3px 0; align-items:flex-start; }}
|
.rem-step {{ display:flex; gap:6px; padding:3px 0; align-items:flex-start; }}
|
||||||
.rem-step .step-num {{ font-weight:700; color:var(--header-blue); min-width:20px; }}
|
.rem-step .step-num {{ font-weight:700; color:#2f5597; min-width:20px; }}
|
||||||
.rem-text {{ margin:4px 0; text-align:justify; color:#374151; }}
|
.rem-text {{ margin:4px 0; text-align:justify; color:#374151; }}
|
||||||
.code-snippet {{ background:#1e293b; color:#e2e8f0; padding:10px 14px; border-radius:8px; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:11px; line-height:1.5; overflow-x:auto; white-space:pre-wrap; word-break:break-all; margin:6px 0; border-left:3px solid var(--header-blue); }}
|
.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 */
|
||||||
.compliance-bar {{ display:flex; align-items:center; gap:10px; margin:6px 0 8px; }}
|
.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-label {{ font-size:11px; font-weight:700; width:100px; white-space:nowrap; }}
|
||||||
.bar {{ flex:1; height:10px; background:#eef2ff; border:1px solid #e5e7eb; border-radius:999px; overflow:hidden; }}
|
.bar {{ flex:1; height:8px; background:#eef2ff; border:1px solid #e5e7eb; border-radius:999px; overflow:hidden; }}
|
||||||
.fill {{ height:100%; border-radius:999px; }}
|
.fill {{ height:100%; border-radius:999px; }}
|
||||||
.fill.bad {{ background:var(--bad); }}
|
.fill.bad {{ background:var(--bad); }}
|
||||||
.fill.warn {{ background:var(--warn); }}
|
.fill.warn {{ background:var(--warn); }}
|
||||||
.fill.ok {{ background:var(--ok); }}
|
.fill.ok {{ background:var(--ok); }}
|
||||||
|
|
||||||
/* Print button */
|
/* Print button */
|
||||||
.print-btn {{ position:fixed; bottom:20px; right:20px; padding:12px 24px; background:var(--header-blue); color:#fff; border:none; border-radius:8px; font-size:14px; font-weight:700; cursor:pointer; box-shadow:0 4px 12px rgba(0,0,0,.3); z-index:999; }}
|
.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:#1e3a6e; }}
|
.print-btn:hover {{ background:#a83a2c; }}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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">
|
<div class="wrap">
|
||||||
|
|
||||||
<!-- COVER -->
|
<!-- COVER -->
|
||||||
<div class="cover">
|
<div class="cover">
|
||||||
<div class="cover-oracle">ORACLE</div>
|
<div class="cover-oracle">ORACLE</div>
|
||||||
<div class="cover-rect"></div>
|
<div class="cover-spacer"></div>
|
||||||
<div class="cover-title">Oracle Cloud Security<br/>Assessment</div>
|
<div class="cover-title">Oracle Cloud Security<br/>Assessment</div>
|
||||||
<div class="cover-subtitle">{esc(tenancy_name)} – Oracle Cloud Infrastructure</div>
|
<div class="cover-subtitle">{esc(tenancy_name)} – Oracle Cloud Infrastructure</div>
|
||||||
<div class="cover-meta">
|
<div class="cover-meta">
|
||||||
@@ -4555,9 +4600,11 @@ table.benchmark .section-header td {{ background:#f1f5f9; font-weight:700; font-
|
|||||||
<div>Copyright © {now.year}, Oracle and/or its affiliates</div>
|
<div>Copyright © {now.year}, Oracle and/or its affiliates</div>
|
||||||
<div>Confidential – Oracle Restricted</div>
|
<div>Confidential – Oracle Restricted</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cover-bottom"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PURPOSE + DISCLAIMER -->
|
<!-- PURPOSE + DISCLAIMER -->
|
||||||
|
<div class="oracle-brand">ORACLE</div>
|
||||||
<div class="text-block">
|
<div class="text-block">
|
||||||
<h2>Purpose Statement</h2>
|
<h2>Purpose Statement</h2>
|
||||||
<p>{esc(PURPOSE_TEXT)}</p>
|
<p>{esc(PURPOSE_TEXT)}</p>
|
||||||
@@ -4568,21 +4615,28 @@ table.benchmark .section-header td {{ background:#f1f5f9; font-weight:700; font-
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- TOC -->
|
<!-- TOC -->
|
||||||
<div class="toc page-break-before">
|
<div class="page-break-before">
|
||||||
<div class="toc-brand">ORACLE</div>
|
<div class="oracle-brand">ORACLE</div>
|
||||||
|
<div class="toc">
|
||||||
<div class="toc-heading">Table of contents</div>
|
<div class="toc-heading">Table of contents</div>
|
||||||
<div class="toc-rule"></div>
|
<div class="toc-rule"></div>
|
||||||
<div class="toc-lines">{toc_lines}</div>
|
<div class="toc-lines">{toc_lines}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SECURITY OVERVIEW -->
|
<!-- SECURITY OVERVIEW -->
|
||||||
<div class="text-block page-break-before">
|
<div class="page-break-before">
|
||||||
|
<div class="oracle-brand">ORACLE</div>
|
||||||
|
<div class="text-block">
|
||||||
<h2>Security Overview</h2>
|
<h2>Security Overview</h2>
|
||||||
<p>{SECURITY_OVERVIEW}</p>
|
<p>{SECURITY_OVERVIEW}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CIS SECURITY ASSESSMENT SUMMARY -->
|
<!-- CIS SECURITY ASSESSMENT SUMMARY -->
|
||||||
<div class="summary-card page-break-before" id="cis-summary">
|
<div class="page-break-before" id="cis-summary">
|
||||||
|
<div class="oracle-brand">ORACLE</div>
|
||||||
|
<div class="summary-card">
|
||||||
<h1>CIS Security Assessment</h1>
|
<h1>CIS Security Assessment</h1>
|
||||||
<div class="sub">CIS Oracle Cloud Infrastructure Foundations Benchmark v3.0.0</div>
|
<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>
|
<p style="margin-bottom:12px">Number of controls per domain and non-compliant controls on tenancy <b>{esc(tenancy_name)}</b>:</p>
|
||||||
@@ -4591,22 +4645,27 @@ table.benchmark .section-header td {{ background:#f1f5f9; font-weight:700; font-
|
|||||||
<tbody>{domains_rows}</tbody>
|
<tbody>{domains_rows}</tbody>
|
||||||
</table>
|
</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 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>
|
</div>
|
||||||
|
|
||||||
<!-- CIS BENCHMARK RECOMMENDATION SUMMARY -->
|
<!-- CIS BENCHMARK RECOMMENDATION SUMMARY -->
|
||||||
<div class="summary-card page-break-before" id="benchmark-table">
|
<div class="page-break-before" id="benchmark-table">
|
||||||
|
<div class="oracle-brand">ORACLE</div>
|
||||||
|
<div class="summary-card">
|
||||||
<h1>CIS Benchmark Recommendation Summary</h1>
|
<h1>CIS Benchmark Recommendation Summary</h1>
|
||||||
<div class="sub">Status per recommendation</div>
|
<div class="sub">Status per recommendation</div>
|
||||||
<table class="benchmark">
|
<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:70px;text-align:center">Status</th></tr></thead>
|
||||||
<tbody>{summary_rows}</tbody>
|
<tbody>{summary_rows}</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- DETAILED SECTIONS -->
|
<!-- DETAILED SECTIONS -->
|
||||||
{sections_html}
|
{sections_html}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</td></tr></tbody></table>
|
||||||
|
|
||||||
<!-- Print/PDF button -->
|
<!-- Print/PDF button -->
|
||||||
<button class="print-btn no-print" onclick="window.print()">🖶 Download PDF</button>
|
<button class="print-btn no-print" onclick="window.print()">🖶 Download PDF</button>
|
||||||
|
|||||||
23
frontend-react/.gitignore
vendored
Normal file
23
frontend-react/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
115
frontend-react/dist/assets/index-BCYp2Z2l.js
vendored
Normal file
115
frontend-react/dist/assets/index-BCYp2Z2l.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
frontend-react/dist/assets/index-D9WRGO1k.css
vendored
Normal file
2
frontend-react/dist/assets/index-D9WRGO1k.css
vendored
Normal file
File diff suppressed because one or more lines are too long
22
frontend-react/dist/index.html
vendored
Normal file
22
frontend-react/dist/index.html
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/app/logo.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AI Agent - Infrastructure & Security Engineer</title>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
|
<script type="module" crossorigin src="/app/assets/index-BCYp2Z2l.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/app/assets/index-D9WRGO1k.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script>
|
||||||
|
// Restore theme before React hydration to avoid flash
|
||||||
|
(function() {
|
||||||
|
var t = localStorage.getItem('theme') || 'light';
|
||||||
|
document.documentElement.className = t;
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
21
frontend-react/dist/logo.svg
vendored
Normal file
21
frontend-react/dist/logo.svg
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" width="128" height="128">
|
||||||
|
<!-- Oracle-style rounded rectangle -->
|
||||||
|
<rect x="2" y="5" width="32" height="26" rx="13" ry="13" fill="rgba(199,70,52,0.08)" stroke="#C74634" stroke-width="2"/>
|
||||||
|
<!-- Robot head -->
|
||||||
|
<rect x="11" y="11" width="14" height="14" rx="4" fill="#fff" stroke="#C74634" stroke-width="0.5"/>
|
||||||
|
<!-- Eyes -->
|
||||||
|
<circle cx="15" cy="17" r="2" fill="#C74634"/>
|
||||||
|
<circle cx="21" cy="17" r="2" fill="#C74634"/>
|
||||||
|
<circle cx="15" cy="16.7" r="0.7" fill="#fff"/>
|
||||||
|
<circle cx="21" cy="16.7" r="0.7" fill="#fff"/>
|
||||||
|
<!-- Mouth -->
|
||||||
|
<rect x="14" y="21" width="8" height="1.5" rx="0.75" fill="#C74634" opacity="0.6"/>
|
||||||
|
<!-- Antenna -->
|
||||||
|
<line x1="18" y1="11" x2="18" y2="6" stroke="#C74634" stroke-width="1.2" stroke-linecap="round"/>
|
||||||
|
<circle cx="18" cy="5.5" r="1.5" fill="#C74634" opacity="0.3" stroke="#C74634" stroke-width="0.8"/>
|
||||||
|
<!-- Ears/signal -->
|
||||||
|
<line x1="11" y1="18" x2="6" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
|
||||||
|
<line x1="25" y1="18" x2="30" y2="18" stroke="#C74634" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
|
||||||
|
<circle cx="5.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
|
||||||
|
<circle cx="30.5" cy="18" r="1" fill="#C74634" opacity="0.4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -577,6 +577,7 @@ export default function ReportsPage() {
|
|||||||
const [showCompliance, setShowCompliance] = useState(false);
|
const [showCompliance, setShowCompliance] = useState(false);
|
||||||
const [complianceReady, setComplianceReady] = useState(false);
|
const [complianceReady, setComplianceReady] = useState(false);
|
||||||
const [complianceGenerating, setComplianceGenerating] = useState(false);
|
const [complianceGenerating, setComplianceGenerating] = useState(false);
|
||||||
|
const compliancePollRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
/* ── Error ── */
|
/* ── Error ── */
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
@@ -645,6 +646,7 @@ export default function ReportsPage() {
|
|||||||
await loadReports();
|
await loadReports();
|
||||||
if (p.status === 'completed') {
|
if (p.status === 'completed') {
|
||||||
setSelectedRid(trackingId);
|
setSelectedRid(trackingId);
|
||||||
|
setShowIframe(true);
|
||||||
}
|
}
|
||||||
setTrackingId(null);
|
setTrackingId(null);
|
||||||
return true; // stop polling
|
return true; // stop polling
|
||||||
@@ -693,6 +695,44 @@ export default function ReportsPage() {
|
|||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, [selectedRid]);
|
}, [selectedRid]);
|
||||||
|
|
||||||
|
/* ── Compliance report polling (useEffect-based, survives re-renders + tab throttle) ── */
|
||||||
|
useEffect(() => {
|
||||||
|
if (!complianceGenerating || !selectedRid) return;
|
||||||
|
let active = true;
|
||||||
|
const check = async () => {
|
||||||
|
try {
|
||||||
|
const s = await reportsApi.complianceReportStatus(selectedRid);
|
||||||
|
if (s.ready && active) {
|
||||||
|
setComplianceReady(true);
|
||||||
|
setComplianceGenerating(false);
|
||||||
|
setShowCompliance(true);
|
||||||
|
}
|
||||||
|
} catch { /* keep polling */ }
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
const id = setInterval(check, 3000);
|
||||||
|
compliancePollRef.current = id;
|
||||||
|
const timeout = setTimeout(() => { clearInterval(id); if (active) setComplianceGenerating(false); }, 300000);
|
||||||
|
return () => { active = false; clearInterval(id); clearTimeout(timeout); compliancePollRef.current = null; };
|
||||||
|
}, [complianceGenerating, selectedRid]);
|
||||||
|
|
||||||
|
/* ── Re-check compliance status when section is expanded ── */
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showCompliance || !selectedRid || complianceReady || complianceGenerating) return;
|
||||||
|
reportsApi.complianceReportStatus(selectedRid)
|
||||||
|
.then((s) => { if (s.ready) setComplianceReady(true); })
|
||||||
|
.catch(() => {});
|
||||||
|
}, [showCompliance, selectedRid, complianceReady, complianceGenerating]);
|
||||||
|
|
||||||
|
const startComplianceGeneration = useCallback(async () => {
|
||||||
|
if (!selectedRid) return;
|
||||||
|
setComplianceGenerating(true);
|
||||||
|
setComplianceReady(false);
|
||||||
|
try {
|
||||||
|
await reportsApi.generateComplianceReport(selectedRid);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}, [selectedRid]);
|
||||||
|
|
||||||
/* ── Handlers ── */
|
/* ── Handlers ── */
|
||||||
const handleRun = async () => {
|
const handleRun = async () => {
|
||||||
if (!ociVal) {
|
if (!ociVal) {
|
||||||
@@ -1299,20 +1339,7 @@ export default function ReportsPage() {
|
|||||||
/>
|
/>
|
||||||
<div className="px-5 py-2 flex justify-end" style={{ borderTop: '1px solid var(--bd)' }}>
|
<div className="px-5 py-2 flex justify-end" style={{ borderTop: '1px solid var(--bd)' }}>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={startComplianceGeneration}
|
||||||
setComplianceGenerating(true);
|
|
||||||
setComplianceReady(false);
|
|
||||||
try {
|
|
||||||
await reportsApi.generateComplianceReport(selectedRid);
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
const poll = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const s = await reportsApi.complianceReportStatus(selectedRid);
|
|
||||||
if (s.ready) { clearInterval(poll); setComplianceReady(true); setComplianceGenerating(false); }
|
|
||||||
} catch { /* keep polling */ }
|
|
||||||
}, 3000);
|
|
||||||
setTimeout(() => { clearInterval(poll); setComplianceGenerating(false); }, 300000);
|
|
||||||
}}
|
|
||||||
disabled={complianceGenerating}
|
disabled={complianceGenerating}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-medium cursor-pointer transition-colors"
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-[.68rem] font-medium cursor-pointer transition-colors"
|
||||||
style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}
|
style={{ background: 'var(--bg3)', color: 'var(--t2)', border: '1px solid var(--bd)' }}
|
||||||
@@ -1332,19 +1359,7 @@ export default function ReportsPage() {
|
|||||||
<ShieldCheck size={32} style={{ color: 'var(--t4)', opacity: 0.5 }} />
|
<ShieldCheck size={32} style={{ color: 'var(--t4)', opacity: 0.5 }} />
|
||||||
<p className="text-[.78rem]" style={{ color: 'var(--t3)' }}>Report ainda não gerado</p>
|
<p className="text-[.78rem]" style={{ color: 'var(--t3)' }}>Report ainda não gerado</p>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={startComplianceGeneration}
|
||||||
setComplianceGenerating(true);
|
|
||||||
try {
|
|
||||||
await reportsApi.generateComplianceReport(selectedRid);
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
const poll = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
const s = await reportsApi.complianceReportStatus(selectedRid);
|
|
||||||
if (s.ready) { clearInterval(poll); setComplianceReady(true); setComplianceGenerating(false); }
|
|
||||||
} catch { /* keep polling */ }
|
|
||||||
}, 3000);
|
|
||||||
setTimeout(() => { clearInterval(poll); setComplianceGenerating(false); }, 300000);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 px-4 py-2 rounded-lg text-[.76rem] font-semibold cursor-pointer"
|
className="flex items-center gap-2 px-4 py-2 rounded-lg text-[.76rem] font-semibold cursor-pointer"
|
||||||
style={{ background: 'var(--gn)', color: '#fff' }}
|
style={{ background: 'var(--gn)', color: '#fff' }}
|
||||||
>
|
>
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user