Files
workshops/QueComanTierra/build_pptx.py
anti ae0104b200 add QueComanTierra LOLBin ransomware workshop
Scripts ofensivos (xargs, tarbulk, noxargs), C2 listener, Falco
detection rules, slides md + pptx, y estructura del workshop.
2026-05-19 09:42:02 -04:00

341 lines
14 KiB
Python

#!/usr/bin/env python3
"""
Genera slides.pptx desde slides.md usando la plantilla Diplomados Ciberseguridad.
Los backgrounds se copian directamente desde los slides del template.
"""
from pptx import Presentation
from pptx.util import Pt, Emu
from pptx.dml.color import RGBColor
from lxml import etree
import copy, re, zipfile, io, tempfile, os
TEMPLATE = "../../Red Team.pptx"
SOURCE = "slides.md"
OUTPUT = "slides.pptx"
# Layouts en la plantilla (por nombre español)
L_TITLE = 0 # Diapositiva de titulo (idx 0=ctrTitle, 1=subTitle)
L_CONTENT = 1 # Titulo y objetos (idx 0=title, 1=content)
L_SECTION = 2 # Encabezado de seccion (idx 0=title, 1=body)
# Slides del template que sirven como fuente de backgrounds
# slide1=portada, slide3=contenido, slide5=seccion divisor
BG_TITLE = 0 # image1.jpg — portada con fondo binario rojo
BG_CONTENT = 2 # image3.jpg — slide normal con logos y barra roja
BG_SECTION = 4 # image5.jpg — divisor rojo degradado
PNS = 'http://schemas.openxmlformats.org/presentationml/2006/main'
ANS = 'http://schemas.openxmlformats.org/drawingml/2006/main'
RNS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
IMG_REL = f'{RNS}/image'
# ─── template limpio ─────────────────────────────────────────────────────────
def make_clean_template(src):
"""Devuelve BytesIO: template sin slides (master/layouts/media intactos)."""
SLIDE_XML = re.compile(r'^ppt/slides/slide\d+\.xml$')
SLIDE_RELS = re.compile(r'^ppt/slides/_rels/slide\d+\.xml\.rels$')
SLIDE_CT = 'application/vnd.openxmlformats-officedocument.presentationml.slide+xml'
SLIDE_REL_TYPE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide'
PKG_NS = 'http://schemas.openxmlformats.org/package/2006/relationships'
PML_NS = 'http://schemas.openxmlformats.org/presentationml/2006/main'
CT_NS = 'http://schemas.openxmlformats.org/package/2006/content-types'
buf = io.BytesIO()
with zipfile.ZipFile(src, 'r') as zin, \
zipfile.ZipFile(buf, 'w', zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
name = item.filename
if SLIDE_XML.match(name) or SLIDE_RELS.match(name):
continue # eliminar slides y sus rels del ZIP
data = zin.read(name)
if name == 'ppt/presentation.xml':
root = etree.fromstring(data)
lst = root.find(f'{{{PML_NS}}}sldIdLst')
if lst is not None:
lst.clear()
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
elif name == 'ppt/_rels/presentation.xml.rels':
root = etree.fromstring(data)
for rel in root.findall(f'{{{PKG_NS}}}Relationship'):
if rel.get('Type') == SLIDE_REL_TYPE:
root.remove(rel)
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
elif name == '[Content_Types].xml':
root = etree.fromstring(data)
for ov in root.findall(f'{{{CT_NS}}}Override'):
if ov.get('ContentType') == SLIDE_CT:
root.remove(ov)
data = etree.tostring(root, xml_declaration=True,
encoding='UTF-8', standalone=True)
zout.writestr(item, data)
buf.seek(0)
return buf
# ─── background copy ──────────────────────────────────────────────────────────
def copy_background(new_slide, template_slide):
"""Copia el <p:bg> del template_slide al new_slide, remapeando el rId de imagen."""
cSld_tmpl = template_slide._element.find(f'{{{PNS}}}cSld')
bg = cSld_tmpl.find(f'{{{PNS}}}bg') if cSld_tmpl is not None else None
if bg is None:
return
blip = bg.find(f'.//{{{ANS}}}blip')
if blip is None:
return
old_rId = blip.get(f'{{{RNS}}}embed')
if not old_rId:
return
# Obtener la imagen del template y relacionarla con el nuevo slide
image_part = template_slide.part.related_part(old_rId)
new_rId = new_slide.part.relate_to(image_part, IMG_REL)
# Clonar <p:bg> y actualizar el rId
new_bg = copy.deepcopy(bg)
new_blip = new_bg.find(f'.//{{{ANS}}}blip')
new_blip.set(f'{{{RNS}}}embed', new_rId)
# Insertar en cSld del nuevo slide (antes de spTree)
new_cSld = new_slide._element.find(f'{{{PNS}}}cSld')
existing_bg = new_cSld.find(f'{{{PNS}}}bg')
if existing_bg is not None:
new_cSld.remove(existing_bg)
new_cSld.insert(0, new_bg)
# ─── helpers de texto ─────────────────────────────────────────────────────────
def clean(text):
text = re.sub(r'\*\*(.*?)\*\*', r'\1', text)
text = re.sub(r'\*(.*?)\*', r'\1', text)
text = re.sub(r'`(.*?)`', r'\1', text)
text = re.sub(r'^\s*>\s?', '', text, flags=re.MULTILINE)
return text.strip()
def fill_placeholder(ph, lines):
tf = ph.text_frame
tf.word_wrap = True
tf.clear()
in_code = False
first_p = True
for line in lines:
if line.startswith('```'):
in_code = not in_code
continue
if re.match(r'^\s*\|?[-:| ]+\|', line):
continue
stripped = line.rstrip()
if not stripped and not in_code:
continue
p = tf.paragraphs[0] if first_p else tf.add_paragraph()
first_p = False
bullet = re.match(r'^(\s*)([-*]|\d+\.) (.+)', stripped)
if bullet and not in_code:
indent = len(bullet.group(1)) // 2
p.level = min(indent + 1, 4)
run = p.add_run()
run.text = clean(bullet.group(3))
elif in_code:
# Eliminar bullet del párrafo
pPr = p._p.get_or_add_pPr()
for child in list(pPr):
if child.tag.split('}')[-1].startswith('bu'):
pPr.remove(child)
pPr.append(etree.SubElement(pPr, f'{{{ANS}}}buNone'))
run = p.add_run()
run.text = stripped.lstrip()
run.font.name = 'Courier New'
run.font.size = Pt(13)
run.font.color.rgb = RGBColor(0xC0, 0x00, 0x00)
else:
heading = re.match(r'^#{2,4} (.+)', stripped)
if heading:
run = p.add_run()
run.text = clean(heading.group(1))
run.font.bold = True
else:
run = p.add_run()
run.text = clean(stripped)
# ─── parser ───────────────────────────────────────────────────────────────────
def parse_slides(path):
with open(path) as f:
raw = f.read()
raw = re.sub(r'^---\n.*?\n---\n', '', raw, flags=re.DOTALL)
blocks = re.split(r'\n---\n', raw)
return [b.strip() for b in blocks if b.strip()]
def classify(block):
lines = block.split('\n')
first = lines[0]
if re.match(r'^# ', first):
title = re.sub(r'^# ', '', first).replace('{.center}', '').strip()
body = [l for l in lines[1:] if l.strip() and not l.startswith('{')]
return ('section', title, body)
if re.match(r'^## \{\.center\}', first) or first.strip() == '## {.center}':
body = [l for l in lines[1:] if l.strip()]
return ('section', '', body)
if re.match(r'^## ', first):
title = re.sub(r'^## ', '', first).strip()
return ('content', title, lines[1:])
return ('content', '', lines)
# ─── tables ───────────────────────────────────────────────────────────────────
def parse_md_table(lines):
"""Separa lineas en (non_table_lines, table_rows).
Ignora pipes dentro de code fences para no confundirlos con tablas."""
is_table = lambda l: bool(re.match(r'\s*\|', l.strip()) and '|' in l)
is_sep = lambda l: bool(re.match(r'^\s*\|?[-:| ]+\|', l))
non_table, rows = [], []
in_code = False
for line in lines:
if line.startswith('```'):
in_code = not in_code
non_table.append(line)
continue
if in_code:
non_table.append(line)
continue
if is_sep(line):
continue
if is_table(line):
cells = [clean(c.strip()) for c in line.strip().strip('|').split('|')]
rows.append(cells)
else:
non_table.append(line)
return non_table, rows
def add_pptx_table(slide, rows, left, top, width, height):
"""Añade una tabla PPTX estilizada en las coordenadas dadas."""
if not rows:
return
n_rows = len(rows)
n_cols = max(len(r) for r in rows)
tbl = slide.shapes.add_table(n_rows, n_cols, left, top, width, height).table
# Distribuir columnas proporcionalmente (col0 más estrecha = herramienta)
if n_cols == 2:
tbl.columns[0].width = int(width * 0.25)
tbl.columns[1].width = int(width * 0.75)
else:
for i in range(n_cols):
tbl.columns[i].width = width // n_cols
RED = RGBColor(0x8B, 0x00, 0x00)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
BLACK = RGBColor(0x00, 0x00, 0x00)
LIGHT = RGBColor(0xF2, 0xF2, 0xF2)
for ri, row in enumerate(rows):
header = (ri == 0)
for ci in range(n_cols):
cell = tbl.cell(ri, ci)
text = row[ci] if ci < len(row) else ''
cell.text = text
cell.fill.solid()
cell.fill.fore_color.rgb = RED if header else (LIGHT if ri % 2 == 0 else WHITE)
for para in cell.text_frame.paragraphs:
for run in para.runs:
run.font.size = Pt(14)
run.font.bold = header
run.font.color.rgb = WHITE if header else BLACK
# ─── builder ──────────────────────────────────────────────────────────────────
def build():
# Template original: solo para extraer backgrounds
tmpl = Presentation(TEMPLATE)
tmpl_slides = list(tmpl.slides)
# Template limpio: sin slides, para construir desde cero
clean_buf = make_clean_template(TEMPLATE)
prs = Presentation(clean_buf)
blocks = parse_slides(SOURCE)
def add(layout_idx, bg_idx):
slide = prs.slides.add_slide(prs.slide_layouts[layout_idx])
copy_background(slide, tmpl_slides[bg_idx])
return slide
# Slide 1: portada
slide = add(L_TITLE, BG_TITLE)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = "Que coman tierra"
elif ph.placeholder_format.idx == 1:
ph.text = "De LOLBINs a ransomware\nDiplomados Ciberseguridad"
# Slides desde markdown
for block in blocks:
kind, title, body_lines = classify(block)
if kind == 'section':
slide = add(L_SECTION, BG_SECTION)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = title
elif ph.placeholder_format.idx == 1 and body_lines:
fill_placeholder(ph, body_lines)
for para in ph.text_frame.paragraphs:
for run in para.runs:
run.font.color.rgb = RGBColor(0x00, 0x00, 0x00)
else:
slide = add(L_CONTENT, BG_CONTENT)
non_tbl, tbl_rows = parse_md_table(body_lines)
for ph in slide.placeholders:
if ph.placeholder_format.idx == 0:
ph.text = title
elif ph.placeholder_format.idx == 1:
if tbl_rows:
# Usar posicion del placeholder para colocar la tabla
sp = ph._element
xfrm = sp.find(f'.//{{{ANS}}}xfrm')
if xfrm is not None:
off = xfrm.find(f'{{{ANS}}}off')
ext = xfrm.find(f'{{{ANS}}}ext')
l = int(off.get('x', 0))
t = int(off.get('y', 0))
w = int(ext.get('cx', 0))
h = int(ext.get('cy', 0))
else:
l, t, w, h = 838200, 1600200, 10515600, 4500000
# Si hay texto además de tabla, reducir altura de tabla
if non_tbl:
fill_placeholder(ph, non_tbl)
t_offset = int(h * 0.45)
add_pptx_table(slide, tbl_rows, l, t + t_offset, w, h - t_offset)
else:
ph.text = '' # vaciar placeholder
add_pptx_table(slide, tbl_rows, l, t, w, h)
else:
fill_placeholder(ph, body_lines)
prs.save(OUTPUT)
print(f"OK: {OUTPUT}{len(prs.slides)} slides.")
if __name__ == '__main__':
build()