feat(cybersecurity): auto-commit con 2 cambios
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,10 @@ name: rotate_capture_flows
|
||||
kind: function
|
||||
lang: py
|
||||
domain: cybersecurity
|
||||
version: "1.0.0"
|
||||
version: "1.1.0"
|
||||
purity: impure
|
||||
signature: "class Rotator — mitmproxy addon loaded via mitmdump -s"
|
||||
description: "Addon de mitmproxy que rota el archivo de captura de flows cada N minutos. Crea archivos .mitm con timestamp en el nombre (traffic-YYYYmmdd-HHMMSS.mitm) y abre uno nuevo cuando vence el intervalo de rotacion. La rotacion ocurre en el evento response, por lo que solo sucede cuando hay trafico activo."
|
||||
description: "Addon de mitmproxy que rota el archivo de captura de flows cada N minutos y aplica retencion (borra capturas viejas por edad y por tamaño total). Crea archivos .mitm con timestamp en el nombre (traffic-YYYYmmdd-HHMMSS.mitm) y abre uno nuevo cuando vence el intervalo de rotacion. La rotacion ocurre en el evento response, por lo que solo sucede cuando hay trafico activo. Al rotar, purga las capturas que superen max_age_days o que hagan que el directorio supere max_total_mb."
|
||||
tags: [mitmproxy, capture, rotate, proxy, web-proxy, cybersecurity, flows, traffic]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
@@ -23,7 +23,13 @@ params:
|
||||
desc: "Minutos por archivo antes de rotar. Se pasa via --set rotate_min=N al invocar mitmdump. Default: 20."
|
||||
- name: capture_dir
|
||||
desc: "Directorio donde se escriben los archivos .mitm rotados. Se pasa via --set capture_dir=/ruta. Default: directorio de trabajo actual."
|
||||
output: "Archivos .mitm con nombre traffic-<YYYYmmdd-HHMMSS>.mitm en capture_dir, cada uno con los flows HTTP capturados durante una ventana de rotate_min minutos."
|
||||
- name: exclude_hosts
|
||||
desc: "Hosts o host:port separados por coma que nunca se graban (ej. la propia UI del proxy). Se pasa via --set exclude_hosts=127.0.0.1:8081,localhost:8081. Default: vacio."
|
||||
- name: max_total_mb
|
||||
desc: "Tope de tamaño total del directorio de capturas en MB. Al rotar, borra las .mitm mas antiguas hasta bajar del limite. Se pasa via --set max_total_mb=2048. Default: 0 (sin limite)."
|
||||
- name: max_age_days
|
||||
desc: "Antiguedad maxima de una captura en dias. Al rotar, borra las .mitm mas viejas que este valor. Se pasa via --set max_age_days=7. Default: 0 (sin limite)."
|
||||
output: "Archivos .mitm con nombre traffic-<YYYYmmdd-HHMMSS>.mitm en capture_dir, cada uno con los flows HTTP capturados durante una ventana de rotate_min minutos. Las capturas viejas se purgan automaticamente segun max_age_days y max_total_mb."
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
@@ -59,7 +65,13 @@ Cuando necesites capturar trafico HTTP/S durante periodos largos y quieras troce
|
||||
- Los flows que no tienen respuesta (timeouts de servidor, errores de conexion antes de recibir headers) no llegan al evento `response` y por tanto no se escriben en el archivo. Para capturarlos tambien, habria que sobrescribir adicionalmente el hook `error(self, flow)`.
|
||||
- Requiere mitmproxy >= 10 en el entorno. Instalable con `uv tool install mitmproxy`.
|
||||
- El directorio `capture_dir` se crea automaticamente con `os.makedirs(..., exist_ok=True)` si no existe.
|
||||
- **Retencion**: la purga (`max_total_mb`, `max_age_days`) solo se ejecuta al rotar, es decir, dentro de `_roll`. Si el trafico cesa, no hay rotacion y por tanto no hay purga hasta el siguiente flujo. El archivo activo nunca es candidato a borrado, asi que el directorio puede quedar hasta ~un archivo por encima del tope `max_total_mb` entre rotaciones. A escala normal (ventanas de 20 min, tope en GB) la desviacion es despreciable. Ambos topes son independientes y se aplican a la vez (lo que se cumpla primero); `0` desactiva cada uno por separado.
|
||||
- El cap `max_total_mb` borra los archivos mas antiguos primero (por `mtime`), no por nombre. Verificado en test: con tope de 1 MB y 8 dias, purga por tamaño y por edad de forma independiente.
|
||||
|
||||
## Notas
|
||||
|
||||
El addon usa la API de opciones de mitmproxy 10+ (`loader.add_option`). En versiones anteriores la API de opciones era distinta. Verificar version con `mitmdump --version` antes de cargar.
|
||||
|
||||
## Capability growth log
|
||||
|
||||
- v1.1.0 (2026-06-02) — anade retencion: opciones `max_total_mb` y `max_age_days` que purgan capturas viejas al rotar (por tamaño total del directorio y por antiguedad). Evita llenar el disco en captura continua. Tambien anade `exclude_hosts` para no grabar la propia UI del proxy.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Load with: mitmdump -s rotate_capture_flows.py --set rotate_min=20 --set capture_dir=/path/to/dir
|
||||
"""
|
||||
|
||||
import glob
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
@@ -28,6 +29,8 @@ class Rotator:
|
||||
self._rotate_min: int = 20
|
||||
self._capture_dir: str = "."
|
||||
self._exclude: set = set()
|
||||
self._max_total_mb: int = 0
|
||||
self._max_age_days: int = 0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# mitmproxy lifecycle hooks
|
||||
@@ -57,6 +60,25 @@ class Rotator:
|
||||
"either the host alone or host:port are dropped silently."
|
||||
),
|
||||
)
|
||||
loader.add_option(
|
||||
name="max_total_mb",
|
||||
typespec=int,
|
||||
default=0,
|
||||
help=(
|
||||
"Retention cap on total size of the capture directory in MB. "
|
||||
"When a new file is rolled, the oldest .mitm files are deleted "
|
||||
"until the total drops below this limit. 0 disables the cap."
|
||||
),
|
||||
)
|
||||
loader.add_option(
|
||||
name="max_age_days",
|
||||
typespec=int,
|
||||
default=0,
|
||||
help=(
|
||||
"Retention cap on capture age in days. On rollover, .mitm files "
|
||||
"older than this are deleted. 0 disables the age cap."
|
||||
),
|
||||
)
|
||||
|
||||
def configure(self, updated) -> None:
|
||||
"""Read option values and ensure the capture directory exists."""
|
||||
@@ -65,6 +87,8 @@ class Rotator:
|
||||
self._exclude = {
|
||||
h.strip() for h in ctx.options.exclude_hosts.split(",") if h.strip()
|
||||
}
|
||||
self._max_total_mb = ctx.options.max_total_mb
|
||||
self._max_age_days = ctx.options.max_age_days
|
||||
if self._capture_dir:
|
||||
os.makedirs(self._capture_dir, exist_ok=True)
|
||||
|
||||
@@ -111,6 +135,68 @@ class Rotator:
|
||||
|
||||
ctx.log.info(f"rotate_capture_flows: opened new capture file {self._current_path}")
|
||||
|
||||
# Enforce retention after opening the new active file, so the file
|
||||
# currently being written is never a deletion candidate.
|
||||
self._enforce_retention()
|
||||
|
||||
def _enforce_retention(self) -> None:
|
||||
"""Delete old capture files that exceed the age or size limits.
|
||||
|
||||
Two independent caps, whichever applies first:
|
||||
- age: files older than ``max_age_days`` are removed.
|
||||
- size: oldest files are removed until the directory total drops
|
||||
below ``max_total_mb``.
|
||||
The currently active file is never deleted. Both caps are skipped
|
||||
when set to 0.
|
||||
"""
|
||||
if self._max_age_days <= 0 and self._max_total_mb <= 0:
|
||||
return
|
||||
try:
|
||||
pattern = os.path.join(self._capture_dir, "traffic-*.mitm")
|
||||
files = [f for f in glob.glob(pattern) if f != self._current_path]
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# Pair each file with its mtime/size once, skipping vanished files.
|
||||
stats = []
|
||||
for f in files:
|
||||
try:
|
||||
st = os.stat(f)
|
||||
stats.append((f, st.st_mtime, st.st_size))
|
||||
except OSError:
|
||||
continue
|
||||
stats.sort(key=lambda t: t[1]) # oldest first
|
||||
|
||||
# 1. Age cap.
|
||||
if self._max_age_days > 0:
|
||||
cutoff = time.time() - self._max_age_days * 86400
|
||||
kept = []
|
||||
for f, mtime, size in stats:
|
||||
if mtime < cutoff:
|
||||
self._safe_remove(f)
|
||||
else:
|
||||
kept.append((f, mtime, size))
|
||||
stats = kept
|
||||
|
||||
# 2. Size cap.
|
||||
if self._max_total_mb > 0:
|
||||
limit = self._max_total_mb * 1024 * 1024
|
||||
total = sum(size for _, _, size in stats)
|
||||
for f, _mtime, size in stats: # oldest first
|
||||
if total <= limit:
|
||||
break
|
||||
if self._safe_remove(f):
|
||||
total -= size
|
||||
|
||||
@staticmethod
|
||||
def _safe_remove(path: str) -> bool:
|
||||
try:
|
||||
os.remove(path)
|
||||
ctx.log.info(f"rotate_capture_flows: pruned old capture {path}")
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
def _close(self) -> None:
|
||||
"""Flush and close the current writer and file handle."""
|
||||
if self._fh is not None:
|
||||
|
||||
Reference in New Issue
Block a user