feat(enrichers): vendoring de funciones Python por enricher (issue 0033b)
Cada enricher con `lang: python` y `uses_functions` no vacio ahora
puede empaquetar las funciones del registry que necesita en
`<enricher>/_vendored/`. El run.py importa de ahi en lugar de
`<registry_root>/python/functions/`, lo que hace al binario
distribuible sin dependencia de un fn_registry montado.
Cambios:
1. tools/vendor_enricher_python.sh
- Lee `uses_functions` del manifest (filtrando IDs `*_py_*`).
- Resuelve `file_path` desde registry.db.
- Copia recursivamente con expansion transitiva: si un fichero
vendorizado importa siblings del mismo dominio, los siblings
tambien se copian (resuelve el caso `extract_iocs.py` que
importa 7 modulos hermanos).
- Genera `.vendor.lock` con `<id> <sha256> <src_path>` por
funcion declarada para auditoria.
- Idempotente — si todos los hashes coinciden, no rehace nada.
2. Manifests actualizados con `uses_functions`:
- fetch_webpage: normalize_url + html_to_markdown
- extract_links: extract_urls
- extract_text_entities: extract_iocs
3. run.py de los 3 enrichers afectados: importan de `_vendored/`
si existe, fallback a `<registry_root>/python/functions/` en
modo dev (mantiene los tests pytest funcionando).
4. app.md: anade `cryptography` a python_runtime_deps porque el
blob `cybersecurity.cybersecurity` lo importa al top.
5. Tests:
- test_vendor_script.py — 6 tests del script: layout correcto,
transitive siblings, lock con SHA256, idempotencia, modulos
importables en aislamiento.
- 16 tests de enrichers existentes pasan via vendoring (no usan
registry_root porque _vendored/ tiene prioridad).
6. Issue 0033b movido a issues/completed/.
Tests: 32/32 verde (16 enrichers + 6 dispatcher + 4 runtime + 6
vendor).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Executable
+187
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env bash
|
||||
# vendor_enricher_python.sh — copia las funciones Python del registry
|
||||
# que un enricher declara en `uses_functions` a su directorio
|
||||
# `_vendored/`. El run.py importa de `_vendored/` en lugar de
|
||||
# `<registry_root>/python/functions/`, lo que hace al binario
|
||||
# distribuible sin acceso al fn_registry.
|
||||
#
|
||||
# Issue 0033b.
|
||||
#
|
||||
# Uso:
|
||||
# tools/vendor_enricher_python.sh <enricher_dir> [<registry_root>]
|
||||
#
|
||||
# Lee `uses_functions` del manifest YAML, filtra IDs `*_py_*`,
|
||||
# resuelve `file_path` desde registry.db, copia los .py y todas las
|
||||
# importaciones siblings dentro del mismo dominio (transitivo).
|
||||
#
|
||||
# Genera `.vendor.lock` con `<id> <sha256> <src_path>` para
|
||||
# auditoria. Idempotente — si los hashes coinciden, no copia.
|
||||
#
|
||||
# Salida:
|
||||
# <enricher_dir>/_vendored/__init__.py
|
||||
# <enricher_dir>/_vendored/<domain>/__init__.py
|
||||
# <enricher_dir>/_vendored/<domain>/<filename>.py
|
||||
# <enricher_dir>/.vendor.lock
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ENR_DIR="${1:?enricher_dir requerido}"
|
||||
REGISTRY_ROOT="${2:-${REGISTRY_ROOT:-$(pwd)}}"
|
||||
|
||||
if [[ ! -f "$ENR_DIR/manifest.yaml" ]]; then
|
||||
echo "ERROR: $ENR_DIR/manifest.yaml no existe" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$REGISTRY_ROOT/registry.db" ]]; then
|
||||
echo "ERROR: $REGISTRY_ROOT/registry.db no existe (REGISTRY_ROOT incorrecto)" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
VENDOR="$ENR_DIR/_vendored"
|
||||
LOCK="$ENR_DIR/.vendor.lock"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Leer uses_functions del manifest (subset YAML soportado por el indexer C++).
|
||||
# Acepta forma inline `[a, b]` o lista en lineas indentadas con `- `.
|
||||
# ----------------------------------------------------------------------------
|
||||
ids=$(awk '
|
||||
/^uses_functions:[[:space:]]*\[/ {
|
||||
line = $0
|
||||
sub(/^uses_functions:[[:space:]]*\[/, "", line)
|
||||
sub(/\].*$/, "", line)
|
||||
gsub(/[",]/, " ", line)
|
||||
print line
|
||||
exit
|
||||
}
|
||||
/^uses_functions:[[:space:]]*$/ { collecting = 1; next }
|
||||
collecting && /^[[:space:]]*-[[:space:]]+/ {
|
||||
sub(/^[[:space:]]*-[[:space:]]+/, "")
|
||||
sub(/[[:space:]]*#.*$/, "")
|
||||
gsub(/[\047"]/, "")
|
||||
print
|
||||
next
|
||||
}
|
||||
collecting && /^[^[:space:]-]/ { collecting = 0 }
|
||||
' "$ENR_DIR/manifest.yaml" | tr ' ' '\n' | awk 'NF' | grep '_py_' || true)
|
||||
|
||||
if [[ -z "$ids" ]]; then
|
||||
# No hay nada que vendorizar — limpiamos vendor/ y lock por si
|
||||
# quedaron de un manifest anterior.
|
||||
rm -rf "$VENDOR" "$LOCK"
|
||||
echo "vendor: $ENR_DIR — sin uses_functions Python"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Para cada ID, obtener file_path de registry.db. Construir lista de
|
||||
# (id, abs_src_path, domain, basename) — la unidad atomica de copia.
|
||||
# ----------------------------------------------------------------------------
|
||||
declare -A SEEN # paths absolutos ya procesados (dedup)
|
||||
declare -A LOCK_NEW # nueva tabla de lock: id -> sha256 src_path
|
||||
|
||||
queue=()
|
||||
for id in $ids; do
|
||||
fp=$(sqlite3 "$REGISTRY_ROOT/registry.db" \
|
||||
"SELECT file_path FROM functions WHERE id='$id';")
|
||||
if [[ -z "$fp" ]]; then
|
||||
echo "WARN: $id no esta en registry.db (skip)" >&2
|
||||
continue
|
||||
fi
|
||||
abs="$REGISTRY_ROOT/$fp"
|
||||
if [[ ! -f "$abs" ]]; then
|
||||
echo "WARN: file_path '$fp' no existe (skip $id)" >&2
|
||||
continue
|
||||
fi
|
||||
queue+=("$id|$abs")
|
||||
done
|
||||
|
||||
if [[ ${#queue[@]} -eq 0 ]]; then
|
||||
rm -rf "$VENDOR" "$LOCK"
|
||||
echo "vendor: $ENR_DIR — ninguna funcion Python resoluble"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Idempotencia: comparar hashes vs lock existente. Si todos coinciden,
|
||||
# salir sin tocar nada.
|
||||
# ----------------------------------------------------------------------------
|
||||
state_hash=""
|
||||
for entry in "${queue[@]}"; do
|
||||
id="${entry%%|*}"
|
||||
abs="${entry#*|}"
|
||||
sha=$(sha256sum "$abs" | cut -d' ' -f1)
|
||||
state_hash+="$id|$sha"$'\n'
|
||||
done
|
||||
state_sha=$(echo -n "$state_hash" | sha256sum | cut -d' ' -f1)
|
||||
|
||||
if [[ -f "$LOCK" ]]; then
|
||||
cur_state=""
|
||||
while IFS= read -r line; do
|
||||
# formato: "<id> <sha256> <path>"
|
||||
eid=$(echo "$line" | awk '{print $1}')
|
||||
esha=$(echo "$line" | awk '{print $2}')
|
||||
cur_state+="$eid|$esha"$'\n'
|
||||
done < "$LOCK"
|
||||
cur_sha=$(echo -n "$cur_state" | sha256sum | cut -d' ' -f1)
|
||||
if [[ "$cur_sha" == "$state_sha" ]]; then
|
||||
echo "vendor: $ENR_DIR — sin cambios (.vendor.lock OK)"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Copia con expansion transitiva de imports siblings dentro del mismo
|
||||
# dominio. Si un .py vendorizado tiene `from X import Y` o
|
||||
# `import X` donde X es un modulo del mismo directorio, X.py tambien
|
||||
# se copia (solo si existe junto al fuente original).
|
||||
# ----------------------------------------------------------------------------
|
||||
rm -rf "$VENDOR"
|
||||
mkdir -p "$VENDOR"
|
||||
touch "$VENDOR/__init__.py"
|
||||
|
||||
copy_with_siblings() {
|
||||
local abs="$1"
|
||||
if [[ -n "${SEEN[$abs]:-}" ]]; then return 0; fi
|
||||
SEEN[$abs]=1
|
||||
|
||||
# Inferir dominio del path: <root>/python/functions/<domain>/<file>.py
|
||||
local rel
|
||||
rel=$(realpath --relative-to="$REGISTRY_ROOT" "$abs")
|
||||
local domain
|
||||
domain=$(echo "$rel" | awk -F/ '{print $(NF-1)}')
|
||||
local fname
|
||||
fname=$(basename "$abs")
|
||||
local dst_dir="$VENDOR/$domain"
|
||||
mkdir -p "$dst_dir"
|
||||
touch "$dst_dir/__init__.py"
|
||||
cp "$abs" "$dst_dir/$fname"
|
||||
|
||||
# Escanear imports siblings: lineas `from <name> import` o
|
||||
# `import <name>` donde <name>.py existe en el mismo dir que abs.
|
||||
local src_dir
|
||||
src_dir=$(dirname "$abs")
|
||||
local sibling_names
|
||||
sibling_names=$(grep -E '^[[:space:]]*(from [a-zA-Z_][a-zA-Z0-9_]+ import|import [a-zA-Z_][a-zA-Z0-9_]+)' "$abs" \
|
||||
| sed -E 's/^[[:space:]]*from ([a-zA-Z_][a-zA-Z0-9_]+).*/\1/; s/^[[:space:]]*import ([a-zA-Z_][a-zA-Z0-9_]+).*/\1/' \
|
||||
| sort -u)
|
||||
for name in $sibling_names; do
|
||||
local sib="$src_dir/$name.py"
|
||||
if [[ -f "$sib" && "$sib" != "$abs" ]]; then
|
||||
copy_with_siblings "$sib"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
> "$LOCK.tmp"
|
||||
for entry in "${queue[@]}"; do
|
||||
id="${entry%%|*}"
|
||||
abs="${entry#*|}"
|
||||
copy_with_siblings "$abs"
|
||||
sha=$(sha256sum "$abs" | cut -d' ' -f1)
|
||||
rel=$(realpath --relative-to="$REGISTRY_ROOT" "$abs")
|
||||
echo "$id $sha $rel" >> "$LOCK.tmp"
|
||||
done
|
||||
|
||||
mv "$LOCK.tmp" "$LOCK"
|
||||
n=$(wc -l < "$LOCK")
|
||||
echo "vendor: $ENR_DIR — $n funcs declaradas, $(find "$VENDOR" -name '*.py' | wc -l) archivos copiados"
|
||||
Reference in New Issue
Block a user