Files
A-Team-Security-Infra-Agent…/backend/services/compliance_docx.py
nogueiraguh 1135e9d6a9 refactor: decompose monolith — app.py 10,600→143 lines, 30+ modules
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.
2026-04-06 15:20:10 -03:00

1001 lines
44 KiB
Python

"""DOCX compliance report generation — camo strip, floating images, professional formatting."""
import os, json, re, io
from pathlib import Path
from datetime import datetime
from typing import Optional, List, Dict, Any
from config import REPORTS, log
from database import db
from services.compliance_gen import _OCI_SERVICE_DESCRIPTIONS, _check_oci_services, _generate_compliance_report
def _generate_camo_strip() -> bytes:
"""Generate a camouflage-style decorative strip PNG (like Oracle PDF reports)."""
from PIL import Image, ImageDraw, ImageFilter
import random, io as _io
W, H = 36, 1500
# Exact colors from the HTML page-strip gradient — olive/sage/gold tones
C1 = (107, 127, 58) # #6b7f3a olive green
C2 = (138, 154, 78) # #8a9a4e sage green
C3 = (74, 103, 65) # #4a6741 forest
C4 = (45, 59, 42) # #2d3b2a dark forest
C5 = (196, 169, 77) # #c4a94d gold
C6 = (139, 109, 59) # #8b6d3b brown/tan
C7 = (61, 92, 58) # #3d5c3a deep green
ALL = [C1, C2, C3, C4, C5, C6, C7]
img = Image.new('RGB', (W, H), C4)
draw = ImageDraw.Draw(img)
random.seed(42)
import math
def _poly(cx, cy, r, sides=5):
pts = []
for i in range(sides):
a = 2 * math.pi * i / sides + random.uniform(-0.5, 0.5)
rx = r * random.uniform(0.5, 1.4)
ry = r * random.uniform(0.8, 2.2)
pts.append((cx + rx * math.cos(a), cy + ry * math.sin(a)))
return pts
# Vertical gradient base (like the CSS linear-gradient)
for y in range(H):
t = y / H
# Cycle through gradient stops
if t < 0.16: c = C1 if t < 0.08 else C2
elif t < 0.32: c = C3 if t < 0.24 else C4
elif t < 0.48: c = C5 if t < 0.40 else C6
elif t < 0.64: c = C7 if t < 0.56 else C1
elif t < 0.80: c = C5 if t < 0.72 else C3
else: c = C4 if t < 0.88 else C2
draw.line([(0, y), (W, y)], fill=c)
# Organic patches on top for camo texture
for _ in range(35):
c = random.choice(ALL)
cx = random.randint(-8, W + 8)
cy = random.randint(-20, H + 20)
draw.polygon(_poly(cx, cy, random.randint(12, 30), random.randint(4, 6)), fill=c)
for _ in range(25):
c = random.choice([C5, C6, C2])
cx = random.randint(0, W)
cy = random.randint(0, H)
draw.polygon(_poly(cx, cy, random.randint(6, 16), random.randint(3, 5)), fill=c)
img = img.filter(ImageFilter.GaussianBlur(radius=1.2))
buf = _io.BytesIO()
img.save(buf, format='PNG')
return buf.getvalue()
def _add_floating_image_to_header(header, image_bytes: bytes, width_emu: int, height_emu: int):
"""Add a floating behind-text image to a DOCX header (positioned at left edge, full page height)."""
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml, OxmlElement
import io as _io
rId, _ = header.part.get_or_add_image(_io.BytesIO(image_bytes))
sw, sh = str(width_emu), str(height_emu)
anchor_xml = f'''
<wp:anchor {nsdecls('wp','a','r','pic')}
distT="0" distB="0" distL="0" distR="0"
simplePos="0" relativeHeight="0" behindDoc="1"
locked="1" layoutInCell="1" allowOverlap="1">
<wp:simplePos x="0" y="0"/>
<wp:positionH relativeFrom="page"><wp:posOffset>0</wp:posOffset></wp:positionH>
<wp:positionV relativeFrom="page"><wp:posOffset>0</wp:posOffset></wp:positionV>
<wp:extent cx="{sw}" cy="{sh}"/>
<wp:wrapNone/>
<wp:docPr id="1" name="CamoStrip"/>
<a:graphic>
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic>
<pic:nvPicPr>
<pic:cNvPr id="0" name="strip.png"/>
<pic:cNvPicPr/>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="{rId}"/>
<a:stretch><a:fillRect/></a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:off x="0" y="0"/>
<a:ext cx="{sw}" cy="{sh}"/>
</a:xfrm>
<a:prstGeom prst="rect"/>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:anchor>'''
anchor_el = parse_xml(anchor_xml)
drawing = OxmlElement('w:drawing')
drawing.append(anchor_el)
# Add to the first run in header paragraph
p = header.paragraphs[0]
runs = p.runs
if runs:
runs[0]._r.append(drawing)
else:
r = p.add_run()
r._r.append(drawing)
def _generate_compliance_docx(rid: str, tenancy: str, file_map: dict) -> bytes:
"""Generate a professionally styled DOCX version of the compliance report matching the HTML/PDF."""
import csv as csvmod, io as _io, re as _re
from datetime import datetime as _dt
from docx import Document
from docx.shared import Inches, Pt, Cm, RGBColor, Emu
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml.ns import qn, nsdecls
from docx.oxml import parse_xml, OxmlElement
import json as _json
rdir = REPORTS / rid
# Read the same processed data used by the HTML report (ensures identical content)
data_path = rdir / "compliance_data.json"
if not data_path.exists():
log.warning(f"DOCX: compliance_data.json not found for {rid} — generate HTML report first")
return None
with open(data_path, "r", encoding="utf-8") as f:
report_data = _json.load(f)
tenancy_name = report_data.get("tenancy_name", tenancy)
extract_date = report_data.get("extract_date", "")
filtered = report_data.get("filtered", [])
sections_data = report_data.get("sections", {})
oci_services = report_data.get("oci_services", [])
file_path_map = report_data.get("file_path_map", file_map)
if not filtered:
return None
now = _dt.utcnow()
months_en = ["January","February","March","April","May","June","July","August","September","October","November","December"]
month_year = f"{months_en[now.month - 1]}, {now.year}"
# ── Exact colors from HTML/PDF spec ──
ORACLE_RED = RGBColor(0xC7, 0x46, 0x34)
HEADER_GREEN = '4A7C59'
FINDINGS_HEADER_GREEN = '4A6741'
ROW_ALT = 'EEF3F0'
GREEN = RGBColor(0x0F, 0x7B, 0x0F)
RED = RGBColor(0xC0, 0x00, 0x00)
MUTED = RGBColor(0x64, 0x74, 0x8B)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
BODY_TEXT = RGBColor(0x1A, 0x1A, 0x1A)
TITLE_DARK = RGBColor(0x11, 0x18, 0x27)
SUBTITLE_GRAY = RGBColor(0x37, 0x41, 0x51)
ORANGE = RGBColor(0xD9, 0x77, 0x06)
# ── Helpers ──
def _cell_shading(cell, color_hex):
shading = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color_hex}"/>')
cell._tc.get_or_add_tcPr().append(shading)
def _set_cell_width(cell, width_inches):
tcPr = cell._tc.get_or_add_tcPr()
tcW = OxmlElement('w:tcW')
tcW.set(qn('w:w'), str(int(width_inches * 1440)))
tcW.set(qn('w:type'), 'dxa')
tcPr.append(tcW)
def _cell_margins(cell, top=40, bottom=40, left=80, right=80):
"""Set cell margins in twips for tighter padding."""
tcPr = cell._tc.get_or_add_tcPr()
tcMar = OxmlElement('w:tcMar')
for side, val in [('top', top), ('bottom', bottom), ('start', left), ('end', right)]:
el = OxmlElement(f'w:{side}')
el.set(qn('w:w'), str(val))
el.set(qn('w:type'), 'dxa')
tcMar.append(el)
tcPr.append(tcMar)
def _cell_text(cell, text, bold=False, color=None, size=Pt(9), align=WD_ALIGN_PARAGRAPH.LEFT, uppercase=False):
cell.text = ""
p = cell.paragraphs[0]
p.alignment = align
p.paragraph_format.space_before = Pt(2)
p.paragraph_format.space_after = Pt(2)
p.paragraph_format.line_spacing = 1.0
display = str(text).upper() if uppercase else str(text)
r = p.add_run(display)
r.font.size = size
r.font.name = 'Calibri'
r.bold = bold
if color:
r.font.color.rgb = color
else:
r.font.color.rgb = BODY_TEXT
def _table_borders(table, color='D0D5DD', size='4'):
tbl = table._tbl
tblPr = tbl.tblPr if tbl.tblPr is not None else OxmlElement('w:tblPr')
borders = OxmlElement('w:tblBorders')
for edge in ('top', 'left', 'bottom', 'right', 'insideH', 'insideV'):
el = OxmlElement(f'w:{edge}')
el.set(qn('w:val'), 'single')
el.set(qn('w:sz'), size)
el.set(qn('w:space'), '0')
el.set(qn('w:color'), color)
borders.append(el)
tblPr.append(borders)
def _set_repeat_header(table):
"""Make first row repeat as header on page breaks (like PDF)."""
tr = table.rows[0]._tr
trPr = tr.get_or_add_trPr()
tblHeader = OxmlElement('w:tblHeader')
trPr.append(tblHeader)
def _no_cell_spacing(table):
"""Remove cell spacing for tight table look."""
tbl = table._tbl
tblPr = tbl.tblPr if tbl.tblPr is not None else OxmlElement('w:tblPr')
spacing = OxmlElement('w:tblCellSpacing')
spacing.set(qn('w:w'), '0')
spacing.set(qn('w:type'), 'dxa')
tblPr.append(spacing)
def _styled_table(doc, headers, data_rows, col_widths=None, header_align=None, header_color=None):
"""Create a table matching the PDF style: green header, alternating rows."""
hdr_clr = header_color or HEADER_GREEN
tbl = doc.add_table(rows=1 + len(data_rows), cols=len(headers))
tbl.autofit = False
tbl.allow_autofit = False
tbl.alignment = WD_TABLE_ALIGNMENT.CENTER
# Set column widths if provided
if col_widths:
for i, w in enumerate(col_widths):
for row in tbl.rows:
_set_cell_width(row.cells[i], w)
# Header row — green background, white text, uppercase
for i, h in enumerate(headers):
cell = tbl.rows[0].cells[i]
_cell_shading(cell, hdr_clr)
_cell_margins(cell)
h_align = WD_ALIGN_PARAGRAPH.CENTER
if header_align and i < len(header_align):
h_align = header_align[i]
elif i == 0:
h_align = WD_ALIGN_PARAGRAPH.LEFT
_cell_text(cell, h, bold=True, color=WHITE, size=Pt(9), align=h_align, uppercase=True)
# Data rows — alternating green tint / white
for ridx, row_data in enumerate(data_rows):
bg = ROW_ALT if ridx % 2 == 0 else 'FFFFFF'
for cidx, val in enumerate(row_data):
cell = tbl.rows[ridx + 1].cells[cidx]
_cell_shading(cell, bg)
_cell_margins(cell)
if isinstance(val, tuple):
txt, bld, clr = val[0], val[1], val[2]
a = WD_ALIGN_PARAGRAPH.CENTER if cidx > 0 and len(str(txt)) < 10 else WD_ALIGN_PARAGRAPH.LEFT
_cell_text(cell, txt, bold=bld, color=clr, size=Pt(9), align=a)
else:
a = WD_ALIGN_PARAGRAPH.CENTER if cidx > 0 and len(str(val)) < 10 else WD_ALIGN_PARAGRAPH.LEFT
_cell_text(cell, val, size=Pt(9), align=a)
_table_borders(tbl)
_no_cell_spacing(tbl)
_set_repeat_header(tbl)
return tbl
def _add_heading_red(doc, text, level=1):
h = doc.add_heading(text, level=level)
for r in h.runs:
r.font.color.rgb = ORACLE_RED
r.font.name = 'Calibri'
return h
def _clean_text(text):
"""Join artificially broken lines into flowing paragraphs.
Preserves: paragraph breaks, numbered lists, bullets, headers."""
if not text:
return text
lines = text.split('\n')
result = []
for line in lines:
line = line.strip()
if not line:
result.append('\n')
continue
starts_new = (
_re.match(r'^\d+[\.\)]\s', line) or
_re.match(r'^[a-z][\.\)]\s', line) or
line.startswith('- ') or line.startswith('* ') or
line.startswith('From ') or
line.startswith('Note:') or line.startswith('See ')
)
if starts_new:
result.append(line)
elif result and result[-1] != '\n' and not result[-1].endswith(('.', ':', '!', '?', ';')):
result[-1] = result[-1] + ' ' + line
else:
result.append(line)
return '\n'.join(result).strip()
def _add_body_para(doc, text, size=Pt(10), color=None, bold=False, italic=False, space_after=Pt(8), space_before=Pt(0), align=WD_ALIGN_PARAGRAPH.JUSTIFY):
"""Add paragraph(s) with consistent body styling. Detects numbered/bullet lists and indents them."""
text = _clean_text(text)
if not text:
return None
last_p = None
for chunk in text.split('\n'):
chunk = chunk.strip()
if not chunk:
continue
# Detect list items: "1. text", "a. text", "- text", "* text"
list_match = _re.match(r'^(\d+[\.\)]|[a-z][\.\)]|-|\*)\s+(.+)', chunk)
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.LEFT if list_match else align
p.paragraph_format.space_before = Pt(1) if list_match else space_before
p.paragraph_format.space_after = Pt(3) if list_match else space_after
p.paragraph_format.line_spacing = 1.25
if list_match:
# Indent list items
p.paragraph_format.left_indent = Inches(0.3)
p.paragraph_format.first_line_indent = Inches(-0.3)
# Number/bullet in bold
r = p.add_run(list_match.group(1) + ' ')
r.font.name = 'Calibri'
r.font.size = size
r.font.color.rgb = RGBColor(0x2F, 0x55, 0x97)
r.bold = True
# Content
r = p.add_run(list_match.group(2))
r.font.name = 'Calibri'
r.font.size = size
r.font.color.rgb = color or BODY_TEXT
else:
r = p.add_run(chunk)
r.font.name = 'Calibri'
r.font.size = size
r.font.color.rgb = color or BODY_TEXT
r.bold = bold
r.italic = italic
last_p = p
return last_p
def _is_code_line(line):
"""Detect if a line is a CLI command or code snippet."""
line = line.strip()
if not line:
return False
# Strong indicators: starts with known CLI commands
cli_starts = ['oci ', 'aws ', 'az ', 'gcloud ', 'terraform ', 'kubectl ',
'curl ', 'grep ', 'cat ', 'echo ', 'export ', 'sudo ', '#!/']
if any(line.startswith(p) for p in cli_starts):
return True
# Continuation lines (start with --flag or -flag=value)
if _re.match(r'^--\w+', line) or _re.match(r'^-\w+=', line):
return True
# Shell constructs
if line in ('do', 'done', 'fi', 'then', 'else', 'esac'):
return True
if line.startswith('for ') and (' in ' in line or line.endswith('do')):
return True
return False
def _is_section_header(line):
"""Detect "From Console:", "From Command Line:", "Audit / Verification:", etc."""
low = line.strip().lower()
return (low.startswith('from console') or low.startswith('from oci console') or
low.startswith('from command') or low.startswith('from cli') or
low.startswith('from the command') or low.startswith('audit') and 'verification' in low)
def _add_code_block(doc, code_text):
"""Add a code block with monospace font and gray background."""
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(6)
p.paragraph_format.space_after = Pt(8)
p.paragraph_format.line_spacing = 1.1
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
pPr = p._p.get_or_add_pPr()
pShd = OxmlElement('w:shd')
pShd.set(qn('w:val'), 'clear')
pShd.set(qn('w:color'), 'auto')
pShd.set(qn('w:fill'), 'F3F4F6')
pPr.append(pShd)
pBdr = OxmlElement('w:pBdr')
for edge_name, color in [('left', '9CA3AF'), ('top', 'E5E7EB'), ('bottom', 'E5E7EB'), ('right', 'E5E7EB')]:
edge = OxmlElement(f'w:{edge_name}')
edge.set(qn('w:val'), 'single')
edge.set(qn('w:sz'), '6' if edge_name == 'left' else '4')
edge.set(qn('w:space'), '6' if edge_name == 'left' else '2')
edge.set(qn('w:color'), color)
pBdr.append(edge)
pPr.append(pBdr)
r = p.add_run(code_text)
r.font.name = 'Consolas'
r.font.size = Pt(8)
r.font.color.rgb = RGBColor(0x1F, 0x29, 0x37)
def _add_remediation_docx(doc, remediation_text):
"""Add remediation text with code blocks and section headers, matching HTML output."""
# Split into paragraphs first (double newline), then process each
paragraphs = _re.split(r'\n{2,}', remediation_text)
for para_text in paragraphs:
para_text = para_text.strip()
if not para_text:
continue
lines = para_text.split('\n')
# Check if entire paragraph is code
non_empty = [l.strip() for l in lines if l.strip()]
if non_empty and all(_is_code_line(l) for l in non_empty):
_add_code_block(doc, '\n'.join(non_empty))
continue
# Mixed content: process line by line
code_buffer = []
text_buffer = []
def flush_code():
if code_buffer:
_add_code_block(doc, '\n'.join(code_buffer))
code_buffer.clear()
def flush_text():
if text_buffer:
joined = ' '.join(text_buffer)
_add_body_para(doc, joined, size=Pt(9), space_after=Pt(6))
text_buffer.clear()
for line in lines:
stripped = line.strip()
if not stripped:
continue
if _is_section_header(stripped):
flush_text()
flush_code()
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(8)
p.paragraph_format.space_after = Pt(4)
r = p.add_run(stripped)
r.bold = True
r.font.size = Pt(10)
r.font.name = 'Calibri'
r.font.color.rgb = BODY_TEXT
elif _is_code_line(stripped):
flush_text()
code_buffer.append(stripped)
else:
flush_code()
# If line ends with sentence punctuation, flush as separate paragraph
text_buffer.append(stripped)
if stripped.endswith(('.', ':', '!', '?', ';')):
flush_text()
flush_code()
flush_text()
def _result_box(doc, tenancy_name, is_compliant, findings="0", total="0"):
"""Add a result box with colored background and left border (matching PDF)."""
p = doc.add_paragraph()
pPr = p._p.get_or_add_pPr()
# Background shading
pShd = OxmlElement('w:shd')
pShd.set(qn('w:val'), 'clear')
pShd.set(qn('w:color'), 'auto')
pShd.set(qn('w:fill'), 'F0FDF4' if is_compliant else 'FEF2F2')
pPr.append(pShd)
# Left border
pBdr = OxmlElement('w:pBdr')
left_b = OxmlElement('w:left')
left_b.set(qn('w:val'), 'single')
left_b.set(qn('w:sz'), '18')
left_b.set(qn('w:space'), '6')
left_b.set(qn('w:color'), '0F7B0F' if is_compliant else 'C00000')
pBdr.append(left_b)
pPr.append(pBdr)
# Padding via indentation
pInd = OxmlElement('w:ind')
pInd.set(qn('w:left'), '120')
pPr.append(pInd)
p.paragraph_format.space_before = Pt(8)
p.paragraph_format.space_after = Pt(8)
run = p.add_run("Result: ")
run.bold = True
run.font.size = Pt(10)
run.font.color.rgb = BODY_TEXT
run.font.name = 'Calibri'
run = p.add_run(f"Tenancy {tenancy_name}: ")
run.bold = True
run.font.size = Pt(10)
run.font.color.rgb = BODY_TEXT
run.font.name = 'Calibri'
if is_compliant:
run = p.add_run("Compliant")
run.font.color.rgb = GREEN
run.bold = True
run.font.size = Pt(10)
run.font.name = 'Calibri'
else:
detail = f" ({findings} of {total} items)" if findings and total else ""
run = p.add_run(f"Non-Compliant{detail}")
run.font.color.rgb = RED
run.bold = True
run.font.size = Pt(10)
run.font.name = 'Calibri'
return p
doc = Document()
# ── Global styles ──
style = doc.styles['Normal']
style.font.name = 'Calibri'
style.font.size = Pt(10)
style.font.color.rgb = BODY_TEXT
style.paragraph_format.space_after = Pt(4)
style.paragraph_format.line_spacing = 1.15
for i in range(1, 4):
hs = doc.styles[f'Heading {i}']
hs.font.color.rgb = ORACLE_RED
hs.font.name = 'Calibri'
doc.styles['Heading 1'].font.size = Pt(22)
doc.styles['Heading 1'].font.bold = True
doc.styles['Heading 2'].font.size = Pt(16)
doc.styles['Heading 3'].font.size = Pt(12)
# ── Page size A4 + Margins ──
sect = doc.sections[0]
sect.page_width = Cm(21.0)
sect.page_height = Cm(29.7)
sect.top_margin = Cm(2.5)
sect.bottom_margin = Cm(2.0)
sect.left_margin = Cm(2.5)
sect.right_margin = Cm(2.5)
# ── Header / Footer ──
hdr = sect.header
hdr.is_linked_to_previous = False
hp = hdr.paragraphs[0]
hp.alignment = WD_ALIGN_PARAGRAPH.LEFT
r = hp.add_run("ORACLE")
r.font.color.rgb = ORACLE_RED
r.font.size = Pt(14)
r.bold = True
r.font.name = 'Calibri'
# Add decorative camouflage strip to header (floating, behind text, left edge)
try:
camo_bytes = _generate_camo_strip()
strip_w = int(1.0 * 360000) # ~1cm in EMU
strip_h = int(29.7 * 360000) # A4 height (297mm) in EMU
_add_floating_image_to_header(hdr, camo_bytes, strip_w, strip_h)
except Exception as e:
log.warning(f"DOCX: failed to add camo strip: {e}")
ftr = sect.footer
ftr.is_linked_to_previous = False
fp = ftr.paragraphs[0]
fp.alignment = WD_ALIGN_PARAGRAPH.CENTER
r = fp.add_run("Confidential \u2013 Oracle Restricted")
r.font.size = Pt(8)
r.font.color.rgb = MUTED
r.font.name = 'Calibri'
# ═══════════════════════════════════════════════
# ── COVER PAGE ──
# ═══════════════════════════════════════════════
# Spacer to push title to center of page
for _ in range(10):
sp = doc.add_paragraph()
sp.paragraph_format.space_before = Pt(0)
sp.paragraph_format.space_after = Pt(0)
sp.paragraph_format.line_spacing = 1.0
# Title — centered, serif font
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(0)
r = p.add_run("CIS Foundations")
r.font.size = Pt(36)
r.font.color.rgb = TITLE_DARK
r.font.name = 'Georgia'
r.bold = False
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(12)
r = p.add_run("Benchmark Assessment")
r.font.size = Pt(36)
r.font.color.rgb = TITLE_DARK
r.font.name = 'Georgia'
r.bold = False
# Subtitle — centered
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(8)
p.paragraph_format.space_after = Pt(8)
r = p.add_run(f"{tenancy} \u2013 Oracle Cloud Infrastructure")
r.font.size = Pt(13)
r.font.color.rgb = SUBTITLE_GRAY
r.font.name = 'Calibri'
# Extra spacer before metadata
for _ in range(6):
sp = doc.add_paragraph()
sp.paragraph_format.space_before = Pt(0)
sp.paragraph_format.space_after = Pt(0)
sp.paragraph_format.line_spacing = 1.0
# Metadata — identical to PDF cover
def _meta_line(doc, text, bold_prefix=None, sz=Pt(10)):
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.paragraph_format.space_before = Pt(1)
p.paragraph_format.space_after = Pt(1)
p.paragraph_format.line_spacing = 1.3
if bold_prefix:
r = p.add_run(f"{bold_prefix} ")
r.bold = True
r.font.size = sz
r.font.color.rgb = BODY_TEXT
r.font.name = 'Calibri'
r = p.add_run(text)
else:
r = p.add_run(text)
r.font.size = sz
r.font.color.rgb = SUBTITLE_GRAY
r.font.name = 'Calibri'
_meta_line(doc, tenancy, bold_prefix="Tenancy:")
_meta_line(doc, f"{month_year}, Version [1.0]", sz=Pt(9))
if extract_date:
_meta_line(doc, extract_date, bold_prefix="Extract date:", sz=Pt(9))
_meta_line(doc, f"Copyright \u00A9 {now.year}, Oracle and/or its affiliates", sz=Pt(9))
_meta_line(doc, "Confidential \u2013 Oracle Restricted", sz=Pt(9))
doc.add_page_break()
# ═══════════════════════════════════════════════
# ── DISCLAIMER ──
# ═══════════════════════════════════════════════
_add_heading_red(doc, "Disclaimer", level=1)
_add_body_para(doc,
'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.',
space_after=Pt(8))
_add_body_para(doc,
'This 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.',
space_after=Pt(8))
# ═══════════════════════════════════════════════
# ── PROFILE DEFINITIONS ──
# ═══════════════════════════════════════════════
_add_heading_red(doc, "Profile Definitions", level=1)
p = doc.add_paragraph()
p.paragraph_format.line_spacing = 1.15
r = p.add_run("Level 1")
r.bold = True
r.font.name = 'Calibri'
r.font.color.rgb = BODY_TEXT
r = p.add_run(" \u2014 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.")
r.font.name = 'Calibri'
r.font.color.rgb = BODY_TEXT
p = doc.add_paragraph()
p.paragraph_format.line_spacing = 1.15
r = p.add_run("Level 2")
r.bold = True
r.font.name = 'Calibri'
r.font.color.rgb = BODY_TEXT
r = p.add_run(" \u2014 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.")
r.font.name = 'Calibri'
r.font.color.rgb = BODY_TEXT
# ═══════════════════════════════════════════════
# ── TABLE OF CONTENTS ──
# ═══════════════════════════════════════════════
doc.add_page_break()
_add_heading_red(doc, "Table of Contents", level=1)
p = doc.add_paragraph()
r = p.add_run()
fld1 = OxmlElement('w:fldChar'); fld1.set(qn('w:fldCharType'), 'begin'); r._r.append(fld1)
inst = OxmlElement('w:instrText'); inst.set(qn('xml:space'), 'preserve'); inst.text = ' TOC \\o "1-3" \\h \\z \\u '; r._r.append(inst)
fld2 = OxmlElement('w:fldChar'); fld2.set(qn('w:fldCharType'), 'separate'); r._r.append(fld2)
ph = OxmlElement('w:r'); pht = OxmlElement('w:t'); pht.text = "Right-click and select 'Update Field' to generate TOC"; ph.append(pht); r._r.append(ph)
fld3 = OxmlElement('w:fldChar'); fld3.set(qn('w:fldCharType'), 'end'); r._r.append(fld3)
# Auto-update fields on open
settings = doc.settings.element
uf = OxmlElement('w:updateFields'); uf.set(qn('w:val'), 'true'); settings.append(uf)
doc.add_page_break()
# ═══════════════════════════════════════════════
# ── SECURITY OVERVIEW ──
# ═══════════════════════════════════════════════
_add_heading_red(doc, "Security Overview", level=1)
_add_body_para(doc,
'Oracle Cloud Infrastructure (OCI) is designed with security as a foundational pillar. '
'The CIS Oracle Cloud Infrastructure Foundations Benchmark provides prescriptive guidance for '
'establishing a secure baseline configuration. This assessment evaluates the tenancy against the '
'benchmark controls across key security domains including Identity and Access Management, Networking, '
'Logging and Monitoring, Object Storage, and other critical infrastructure services.',
space_after=Pt(8))
_add_body_para(doc,
'The following sections present a summary of findings by domain, a detailed benchmark recommendation '
'summary, and individual findings with remediation guidance for non-compliant controls.',
space_after=Pt(8))
doc.add_page_break()
# ═══════════════════════════════════════════════
# ── DOMAINS SUMMARY TABLE (data from compliance_data.json — identical to HTML) ──
# ═══════════════════════════════════════════════
_add_heading_red(doc, "CIS Security Assessment", level=1)
_add_body_para(doc, f"Number of controls per domain and non-compliant controls on tenancy {tenancy}:",
space_after=Pt(8), align=WD_ALIGN_PARAGRAPH.LEFT)
domain_data = [
(sec, str(sd["total"]), (str(sd["failed"]), True, RED), (str(sd["passed"]), True, GREEN))
for sec, sd in sections_data.items()
]
_styled_table(doc, ["DOMAINS", "TOTAL CONTROLS", "FAILED", "PASSED"], domain_data,
col_widths=[3.5, 1.2, 0.9, 0.9])
# Totals summary line
total_all = sum(sd["total"] for sd in sections_data.values())
total_passed = sum(sd["passed"] for sd in sections_data.values())
total_failed = total_all - total_passed
pct = round(total_passed / total_all * 100) if total_all else 0
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(10)
p.paragraph_format.space_after = Pt(4)
r = p.add_run(f"Total: {total_all} controls | ")
r.font.size = Pt(10)
r.font.color.rgb = BODY_TEXT
r.font.name = 'Calibri'
r = p.add_run(f"Passed: {total_passed} ({pct}%)")
r.font.color.rgb = GREEN
r.font.size = Pt(10)
r.bold = True
r.font.name = 'Calibri'
r = p.add_run(" | ")
r.font.size = Pt(10)
r.font.color.rgb = BODY_TEXT
r.font.name = 'Calibri'
r = p.add_run(f"Failed: {total_failed}")
r.font.color.rgb = RED
r.font.size = Pt(10)
r.bold = True
r.font.name = 'Calibri'
doc.add_page_break()
# ═══════════════════════════════════════════════
# ── BENCHMARK SUMMARY TABLE ──
# ═══════════════════════════════════════════════
_add_heading_red(doc, "CIS Benchmark Recommendation Summary", level=1)
bench_data = []
for r in filtered:
rec = r.get("Recommendation #", "")
title_txt = r.get("Title", "")
level = r.get("Level", "")
compliant = r.get("__compliant", False)
st_text = "OK" if compliant else "FAILED"
st_color = GREEN if compliant else RED
bench_data.append((rec, title_txt, f"L{level}" if level else "", (st_text, True, st_color)))
_styled_table(doc, ["Item", "Description", "Level", "Status"], bench_data,
col_widths=[0.6, 4.0, 0.5, 0.8])
doc.add_page_break()
# ═══════════════════════════════════════════════
# ── DETAILED FINDINGS ──
# ═══════════════════════════════════════════════
for sec_name, sd in sections_data.items():
_add_heading_red(doc, sec_name, level=1)
for r in sd["items"]:
compliant = r.get("__compliant", False)
rec = r.get("Recommendation #", "")
title_txt = r.get("Title", "")
level = r.get("Level", "")
findings = r.get("Findings", "0")
total = r.get("Total", "0")
csv_remediation = (r.get("Remediation", "") or "").strip()
rag_data = r.get("__rag_remediation", {})
if isinstance(rag_data, str):
rag_data = {"description": "", "rationale": "", "remediation": rag_data}
rag_desc = (rag_data.get("description", "") or "").strip() if isinstance(rag_data, dict) else ""
rag_rationale = (rag_data.get("rationale", "") or "").strip() if isinstance(rag_data, dict) else ""
rag_rem = (rag_data.get("remediation", "") or "").strip() if isinstance(rag_data, dict) else ""
# Recommendation heading in Oracle Red
_add_heading_red(doc, f"{rec} {title_txt}", level=2)
# Level badge
if level:
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(0)
p.paragraph_format.space_after = Pt(4)
r_run = p.add_run(f" Level {level} ")
r_run.font.size = Pt(8)
r_run.bold = True
r_run.font.color.rgb = MUTED
r_run.font.name = 'Calibri'
rPr = r_run._r.get_or_add_rPr()
shd = OxmlElement('w:shd')
shd.set(qn('w:val'), 'clear')
shd.set(qn('w:color'), 'auto')
shd.set(qn('w:fill'), 'E8E8E8')
rPr.append(shd)
# Result box with colored background and left border
_result_box(doc, tenancy_name, compliant, findings, total)
# Description (from RAG — same as HTML)
if not compliant and rag_desc:
_add_heading_red(doc, "Description:", level=3)
_add_body_para(doc, rag_desc, size=Pt(9), space_after=Pt(4))
# Rationale (from RAG — same as HTML)
if not compliant and rag_rationale:
_add_heading_red(doc, "Rationale:", level=3)
_add_body_para(doc, rag_rationale, size=Pt(9), space_after=Pt(4))
# Remediation (from RAG or CSV — same as HTML, with code blocks)
remediation = rag_rem or csv_remediation
if not compliant and remediation:
_add_heading_red(doc, "Remediation:", level=3)
_add_remediation_docx(doc, remediation)
# Affected Resources table
if not compliant:
csv_filename = (r.get("Filename", "") or "").strip().strip('"').strip("'")
if csv_filename and csv_filename in file_path_map:
csv_file_path = Path(file_path_map[csv_filename])
if csv_file_path.exists():
try:
with open(csv_file_path, "r", encoding="utf-8") as cf:
csv_rows = list(csvmod.DictReader(cf))
if csv_rows:
cols = [c for c in csv_rows[0].keys() if c.lower() not in
("extract_date", "deep_link", "domain_deeplink", "defined_tags",
"domain_id", "external_identifier", "freeform_tags", "system_tags")][:5]
res_data = []
for crow in csv_rows[:10]:
row_vals = []
for c in cols:
val = crow.get(c, "")
if val.startswith("=HYPERLINK"):
m = _re.search(r',\s*"([^"]+)"', val)
val = m.group(1) if m else ""
if val.startswith("ocid1.") and len(val) > 30:
val = val[:20] + "..." + val[-8:]
if len(val) > 50:
val = val[:47] + "..."
row_vals.append(val)
res_data.append(tuple(row_vals))
if res_data:
_add_heading_red(doc, "Affected Resources:", level=3)
n = len(cols)
cw = [6.5 / n] * n
_styled_table(doc,
[c.replace("_", " ").title() for c in cols],
res_data,
col_widths=cw,
header_color=FINDINGS_HEADER_GREEN)
if len(csv_rows) > 10:
p = doc.add_paragraph(f"Showing 10 of {len(csv_rows)} resources.")
p.runs[0].font.size = Pt(8)
p.runs[0].font.color.rgb = MUTED
except Exception as e:
log.warning(f"DOCX: resource table error for {csv_filename}: {e}")
# ═══════════════════════════════════════════════
# ── OCI SERVICES (from same data as HTML report) ──
# ═══════════════════════════════════════════════
try:
if oci_services:
svc_data = []
for svc in oci_services:
st = svc.get("status", "WARNING")
desc = svc.get("description", "")
st_color = GREEN if st == "OK" else ORANGE
svc_data.append(((st, True, st_color), svc["service"], desc))
doc.add_page_break()
_add_heading_red(doc, "OCI SERVICES", level=1)
_add_heading_red(doc, "Recommendations:", level=2)
_add_body_para(doc,
"The recommended services are already enabled. This way we recommend further development of their usage:",
space_after=Pt(8), align=WD_ALIGN_PARAGRAPH.LEFT)
_styled_table(doc, ["STATUS", "SERVICE", "Description"], svc_data,
col_widths=[0.8, 2.0, 3.7])
# Service detail descriptions
for svc in oci_services:
info = _OCI_SERVICE_DESCRIPTIONS.get(svc["service"], {})
if not info:
continue
_add_heading_red(doc, info.get("title", svc["service"]), level=2)
desc = info.get("description", "")
for para in desc.split("\n"):
para = para.strip()
if para:
_add_body_para(doc, para, space_after=Pt(4))
rec_text = info.get("recommendation", "")
if rec_text:
p = doc.add_paragraph()
p.paragraph_format.line_spacing = 1.15
r = p.add_run("Recommendation: ")
r.bold = True
r.font.name = 'Calibri'
r.font.color.rgb = BODY_TEXT
r = p.add_run(rec_text)
r.font.name = 'Calibri'
r.font.color.rgb = BODY_TEXT
# Cloud Guard recommendations table
cg_recs = svc.get("recommendations", [])
if cg_recs:
cg_data = [(cr["name"], (str(cr["count"]), True, None)) for cr in cg_recs]
_styled_table(doc, ["Recommendations", "Total"], cg_data,
col_widths=[5.5, 0.8])
except Exception as e:
log.warning(f"DOCX: OCI services section error: {e}")
# ═══════════════════════════════════════════════
# ── BACK PAGE ──
# ═══════════════════════════════════════════════
doc.add_page_break()
_add_heading_red(doc, "Connect with us", level=1)
_add_body_para(doc,
"Call +1.800.ORACLE1 or visit oracle.com.\nOutside North America, find your local office at: oracle.com/contact.",
space_after=Pt(12), align=WD_ALIGN_PARAGRAPH.LEFT)
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(8)
p.paragraph_format.space_after = Pt(4)
for icon, link in [("Blog", "blogs.oracle.com"), ("Facebook", "facebook.com/oracle"), ("Twitter", "twitter.com/oracle")]:
r = p.add_run(f" {icon}: {link} ")
r.font.size = Pt(9)
r.font.color.rgb = MUTED
r.font.name = 'Calibri'
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(24)
p.paragraph_format.space_after = Pt(0)
r = p.add_run(
f"Copyright \u00A9 {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. "
"Oracle, Java, MySQL, and NetSuite are registered trademarks of Oracle and/or its affiliates."
)
r.font.size = Pt(7)
r.font.color.rgb = MUTED
r.font.name = 'Calibri'
buf = _io.BytesIO()
doc.save(buf)
return buf.getvalue()