Scripts ofensivos (xargs, tarbulk, noxargs), C2 listener, Falco detection rules, slides md + pptx, y estructura del workshop.
341 lines
14 KiB
Python
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()
|