Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02eed13913 | |||
| 5bbe45ca30 | |||
| 58c4bc5f05 | |||
| b837b8281a | |||
| 73e2f688b6 |
@@ -0,0 +1,83 @@
|
||||
# /full-git-pull — Pull de fn_registry + todos los sub-repos + submodules + fn sync
|
||||
|
||||
Trae los últimos cambios del remote para el repo principal `fn_registry`, todos los sub-repos git anidados, y los submodules de `cpp/vendor/`. Después regenera `registry.db` y corre `fn sync` para tirar de la metadata del `registry_api` (apps, projects, analysis, vaults, pc_locations registrados desde otros PCs).
|
||||
|
||||
## Argumento
|
||||
|
||||
`$ARGUMENTS` — sin uso, ignorar.
|
||||
|
||||
## Pasos
|
||||
|
||||
### 1. Descubrir repos
|
||||
|
||||
```bash
|
||||
cd /home/egutierrez/fn_registry
|
||||
REPOS=$(find . -name ".git" -type d \
|
||||
-not -path "./.git/*" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-not -path "*/.venv/*" \
|
||||
-not -path "*/cpp/vendor/*" \
|
||||
-not -path "*/cpp/build/*" \
|
||||
-not -path "*/sources/*" \
|
||||
-not -path "*/temp/*" 2>/dev/null | sed 's|/.git$||')
|
||||
REPOS=". $REPOS"
|
||||
```
|
||||
|
||||
### 2. Para cada repo: stash si dirty, pull --ff-only, pop
|
||||
|
||||
```bash
|
||||
for r in $REPOS; do
|
||||
( cd "$r" \
|
||||
&& DIRTY=$(git status --porcelain | wc -l) \
|
||||
&& if [ "$DIRTY" -gt 0 ]; then
|
||||
git stash push -m "auto-stash before /full-git-pull" --include-untracked >/dev/null
|
||||
STASHED=1
|
||||
else
|
||||
STASHED=0
|
||||
fi \
|
||||
&& git fetch origin 2>&1 | tail -1 \
|
||||
&& git pull --ff-only 2>&1 | tail -3 \
|
||||
&& if [ "$STASHED" = "1" ]; then
|
||||
git stash pop 2>&1 | tail -3
|
||||
fi
|
||||
)
|
||||
done
|
||||
```
|
||||
|
||||
- Si `--ff-only` falla por divergencia, abortar el pull de ese repo y reportar (no rebasear sin permiso).
|
||||
- Si `stash pop` produce conflictos, **avisar** y dejar el conflicto al usuario; no resolverlo automáticamente.
|
||||
|
||||
### 3. Submodules del repo principal
|
||||
|
||||
```bash
|
||||
git submodule update --init --recursive 2>&1 | tail -10
|
||||
```
|
||||
|
||||
### 4. Regenerar registry.db local
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=1 ./fn index 2>&1 | tail -3
|
||||
```
|
||||
|
||||
### 5. fn sync con credenciales de pass
|
||||
|
||||
```bash
|
||||
USER=$(pass registry/basicauth-user | head -1)
|
||||
PASSWD=$(pass registry/basicauth-pass | head -1)
|
||||
TOKEN=$(pass registry/api-token | head -1)
|
||||
export FN_REGISTRY_API="https://${USER}:${PASSWD}@registry.organic-machine.com"
|
||||
export REGISTRY_API_TOKEN="$TOKEN"
|
||||
./fn sync
|
||||
```
|
||||
|
||||
Si `pass` falla → gpg-agent locked, pedir al usuario `pass show registry/api-token` en su terminal real.
|
||||
|
||||
### 6. Resumen
|
||||
|
||||
Tabla concisa: por repo, commits pulleados o "ya estaba al día"; submodules actualizados; result de `fn index`; result de `fn sync`.
|
||||
|
||||
## Notas
|
||||
|
||||
- Pull solo es fast-forward — nunca rebase ni merge automático.
|
||||
- Si el repo principal pulleó cambios y eliminó archivos referenciados por sub-repos (raro), el usuario debe resolverlo manualmente.
|
||||
- `fn index` se corre **antes** de `fn sync` para que las locations locales reflejen el estado actual.
|
||||
@@ -0,0 +1,80 @@
|
||||
# /full-git-push — Push de fn_registry + todos los sub-repos + fn sync
|
||||
|
||||
Pushea el repo principal `fn_registry` y todos los sub-repos git anidados (apps externalizadas como `registry_dashboard`, projects con repo propio, etc.), y luego ejecuta `fn sync` para empujar la metadata no regenerable (proposals, apps, projects, analysis, vaults, pc_locations) al `registry_api`.
|
||||
|
||||
## Argumento
|
||||
|
||||
`$ARGUMENTS` — opcional. Si se pasa texto, se usa como mensaje de commit por defecto cuando algún repo tenga cambios sin commitear y el usuario apruebe commitear durante el flujo. Sin argumento, se pregunta el mensaje al detectar dirty tree.
|
||||
|
||||
## Pasos
|
||||
|
||||
### 1. Descubrir repos git en el workspace
|
||||
|
||||
```bash
|
||||
cd /home/egutierrez/fn_registry
|
||||
REPOS=$(find . -name ".git" -type d \
|
||||
-not -path "./.git/*" \
|
||||
-not -path "*/node_modules/*" \
|
||||
-not -path "*/.venv/*" \
|
||||
-not -path "*/cpp/vendor/*" \
|
||||
-not -path "*/cpp/build/*" \
|
||||
-not -path "*/sources/*" \
|
||||
-not -path "*/temp/*" 2>/dev/null | sed 's|/.git$||')
|
||||
# Añadir la raíz al principio
|
||||
REPOS=". $REPOS"
|
||||
```
|
||||
|
||||
### 2. Para cada repo, mostrar estado
|
||||
|
||||
```bash
|
||||
for r in $REPOS; do
|
||||
echo "=== $r ==="
|
||||
( cd "$r" && git status -sb && echo "" )
|
||||
done
|
||||
```
|
||||
|
||||
### 3. Manejar dirty trees
|
||||
|
||||
- Si **algún repo** tiene cambios sin commitear: lista los archivos al usuario y **pregunta** qué hacer:
|
||||
- (a) commitear todo con un mensaje (usar `$ARGUMENTS` si está, si no preguntar)
|
||||
- (b) stashear y seguir solo con los commits ahead
|
||||
- (c) abortar
|
||||
- Nunca commitear sin permiso explícito.
|
||||
|
||||
### 4. Push de cada repo
|
||||
|
||||
```bash
|
||||
for r in $REPOS; do
|
||||
( cd "$r" \
|
||||
&& BRANCH=$(git rev-parse --abbrev-ref HEAD) \
|
||||
&& if git rev-parse --abbrev-ref --symbolic-full-name @{u} >/dev/null 2>&1; then
|
||||
git push origin "$BRANCH" 2>&1 | tail -3
|
||||
else
|
||||
echo "[$r] no upstream para '$BRANCH' — saltado"
|
||||
fi
|
||||
)
|
||||
done
|
||||
```
|
||||
|
||||
### 5. fn sync con credenciales de pass
|
||||
|
||||
```bash
|
||||
USER=$(pass registry/basicauth-user | head -1)
|
||||
PASSWD=$(pass registry/basicauth-pass | head -1)
|
||||
TOKEN=$(pass registry/api-token | head -1)
|
||||
export FN_REGISTRY_API="https://${USER}:${PASSWD}@registry.organic-machine.com"
|
||||
export REGISTRY_API_TOKEN="$TOKEN"
|
||||
./fn sync
|
||||
```
|
||||
|
||||
Si `pass` falla con "decryption failed" → gpg-agent locked. Pedir al usuario que ejecute `pass show registry/api-token` en su terminal real (Bash tool no tiene TTY) y reintentar.
|
||||
|
||||
### 6. Resumen
|
||||
|
||||
Imprimir tabla concisa: para cada repo, branch, commits pusheados o "ya estaba al día". Y resultado de `fn sync` (sent / received / imported).
|
||||
|
||||
## Notas
|
||||
|
||||
- Es responsabilidad del comando **pushear**, no decidir qué commitear. Solo commitea si el usuario lo aprueba explícitamente.
|
||||
- Los submodules del directorio `cpp/vendor/` (imgui, implot, glfw, tracy, implot3d) se ignoran (son mirrors upstream, no se pushean desde aquí).
|
||||
- Si una rama va `behind` el remote, abortar el push de ese repo y avisar para correr `/full-git-pull` primero.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 0037 — IoC regex extractor (cybersecurity)
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0037 |
|
||||
| **Estado** | pendiente |
|
||||
| **Prioridad** | alta |
|
||||
| **Tipo** | feature — Python (`python/functions/cybersecurity/`) |
|
||||
|
||||
## Dependencias
|
||||
|
||||
Ninguna. Es la pieza de mayor ROI del plan: regex puros, sin modelos, 100% precision para IoCs tecnicos. Complementa (no sustituye) a GLiNER, que es malo para identificadores tecnicos.
|
||||
|
||||
**Desbloquea:** mejor precision del pipeline hibrido (0040). Permite filtrar IoCs antes del paso LLM/NER en cualquier flujo OSINT.
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Familia de funciones puras que extraen IoCs (Indicators of Compromise) de texto via regex. Cada una retorna `list[dict]` con `value`, `start`, `end` y `type` para que el caller pueda construir `EntityCandidate` o `triple` sin reparsing.
|
||||
|
||||
## Funciones a crear
|
||||
|
||||
| Function ID | Que extrae |
|
||||
|---|---|
|
||||
| `extract_iocs_py_cybersecurity` | Pipeline pure: corre todos los extractors abajo y devuelve lista unificada con `type` |
|
||||
| `extract_ip_addresses_py_cybersecurity` | IPv4 + IPv6, valida rangos (no `999.999.999.999`) |
|
||||
| `extract_emails_py_cybersecurity` | RFC 5322 simplificado |
|
||||
| `extract_domains_py_cybersecurity` | FQDNs con TLD valido (lista compilada) |
|
||||
| `extract_file_hashes_py_cybersecurity` | MD5 (32 hex), SHA1 (40), SHA256 (64), SHA512 (128) — devuelve `algorithm` |
|
||||
| `extract_crypto_wallets_py_cybersecurity` | BTC (legacy + bech32), ETH (0x + 40 hex con checksum opcional) |
|
||||
| `extract_cve_ids_py_cybersecurity` | `CVE-YYYY-NNNN+` |
|
||||
| `extract_mac_addresses_py_cybersecurity` | `xx:xx:xx:xx:xx:xx` y `xx-xx-xx-xx-xx-xx` |
|
||||
| `extract_phone_numbers_py_cybersecurity` | E.164 + formatos comunes ES/EU |
|
||||
|
||||
`extract_urls_py_cybersecurity` ya existe — no duplicar.
|
||||
|
||||
## Contrato
|
||||
|
||||
```python
|
||||
def extract_<ioc_type>(text: str) -> list[dict]:
|
||||
"""
|
||||
Returns:
|
||||
[{"value": str, "start": int, "end": int, "type": "<ioc_type>"}, ...]
|
||||
"""
|
||||
```
|
||||
|
||||
`extract_iocs_py_cybersecurity` (pipeline) compone los anteriores y unifica resultados:
|
||||
```python
|
||||
def extract_iocs(
|
||||
text: str,
|
||||
types: list[str] | None = None, # None = todos
|
||||
) -> list[dict]
|
||||
```
|
||||
|
||||
## Pureza
|
||||
|
||||
Todas `purity: pure`. Solo regex compilado y validacion estructural. Sin red, sin disco, deterministas.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- 9 archivos `.py` + 9 `.md` en `python/functions/cybersecurity/`
|
||||
- 1 archivo `.py` adicional para el pipeline `extract_iocs.py`
|
||||
- Tests unitarios en `python/functions/cybersecurity/tests/test_extract_iocs.py` con corpus pequeño de positivos y negativos por tipo
|
||||
- Frontmatter completo: `params`, `output`, `domain: cybersecurity`, `purity: pure`
|
||||
|
||||
## Validacion
|
||||
|
||||
```bash
|
||||
./fn run extract_iocs_py_cybersecurity # corre tests
|
||||
./fn show extract_iocs_py_cybersecurity # verifica frontmatter
|
||||
```
|
||||
|
||||
Bench informal: 1 MB de texto < 100 ms por extractor en CPU.
|
||||
|
||||
## Notas
|
||||
|
||||
- No validar TLDs con DNS — solo lista estatica de TLDs validos (publicsuffix opcional pero no obligatorio).
|
||||
- Devolver offsets `start`/`end` siempre — los necesitan los issues 0038-0040 para reconciliar con spans de GLiNER.
|
||||
- IPs privadas (10.x, 192.168.x) se extraen igual; el filtrado de relevancia es del caller, no del extractor.
|
||||
@@ -0,0 +1,82 @@
|
||||
# 0038 — GLiNER entity extractor (zero-shot NER)
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0038 |
|
||||
| **Estado** | pendiente |
|
||||
| **Prioridad** | alta |
|
||||
| **Tipo** | feature — Python (`python/functions/datascience/`) |
|
||||
|
||||
## Dependencias
|
||||
|
||||
Recomendado leer `extract_entities_llm_py_datascience`, `entity_candidate_py_datascience`, `build_entity_schema_prompt_py_datascience`. El contrato debe ser **drop-in** con la version LLM.
|
||||
|
||||
**Desbloquea:** 0039 (GLiREL), 0040 (pipeline hibrido). Permite extraccion masiva sin coste por token.
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Wrapper sobre GLiNER (`urchade/gliner_multi-v2.1` por defecto, multilingue ES/EN) que extrae entidades zero-shot a partir del mismo `entity_schema: list[dict]` que ya consume `extract_entities_llm`. Misma firma, mismo retorno (`list[EntityCandidate]`), 50–200x mas rapido y sin coste por token.
|
||||
|
||||
## Funciones a crear
|
||||
|
||||
| Function ID | Rol |
|
||||
|---|---|
|
||||
| `gliner_load_model_py_datascience` | Carga + cachea el modelo. `purity: impure`, `kind: function` |
|
||||
| `extract_entities_gliner_py_datascience` | Extractor por chunk. `purity: impure`, mismo contrato que la version LLM |
|
||||
|
||||
## Contrato
|
||||
|
||||
```python
|
||||
# gliner_load_model.py
|
||||
def gliner_load_model(
|
||||
model_name: str = "urchade/gliner_multi-v2.1",
|
||||
device: str = "auto", # "auto" | "cpu" | "cuda" | "cuda:0"
|
||||
) -> "GLiNER": ...
|
||||
|
||||
# extract_entities_gliner.py
|
||||
def extract_entities_gliner(
|
||||
text: str,
|
||||
entity_schema: list[dict], # mismo formato que extract_entities_llm
|
||||
model: "GLiNER", # inyectado, cargado una sola vez
|
||||
threshold: float = 0.5,
|
||||
flat_ner: bool = True, # False = nested entities
|
||||
) -> list[EntityCandidate]
|
||||
```
|
||||
|
||||
`entity_schema` se traduce internamente: cada `{"name": "person", "label": "Person", ...}` se convierte al label que GLiNER espera. `EntityCandidate` se rellena con:
|
||||
- `name` = span text
|
||||
- `type_ref` = entity name del schema
|
||||
- `type_label` = label legible del schema
|
||||
- `confidence` = score de GLiNER
|
||||
- `attributes["start"]`, `attributes["end"]` = offsets en el chunk
|
||||
- `source_chunk_indices` lo setea el caller (igual que en la version LLM)
|
||||
|
||||
## Pureza
|
||||
|
||||
`gliner_load_model`: impure (lee disco/red la primera vez, descarga ~200 MB de HF).
|
||||
`extract_entities_gliner`: impure (modelo es estado externo) — `error_type: error_go_core` siguiendo la regla del registry.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- `python/functions/datascience/gliner_load_model.py` + `.md`
|
||||
- `python/functions/datascience/extract_entities_gliner.py` + `.md`
|
||||
- Tests en `python/functions/datascience/tests/test_extract_entities_gliner.py` con un corpus minimo (3-5 textos cortos ES/EN, schema con 4-5 tipos).
|
||||
- `pyproject.toml`: añadir `gliner>=0.2.13` como dep opcional bajo `[project.optional-dependencies]` `nlp = ["gliner", ...]`. NO añadir a deps principales para no inflar `.venv` de quien no use NER.
|
||||
- Documentar en el `.md`: `pip install gliner` (o `uv pip install gliner`), tamaño del modelo, latencia esperada CPU vs GPU.
|
||||
|
||||
## Validacion
|
||||
|
||||
1. `./fn run extract_entities_gliner_py_datascience` corre los tests.
|
||||
2. Bench manual: medir tokens/seg en un texto de 10 KB con 8 labels, CPU y GPU si hay.
|
||||
3. Compararlo con `extract_entities_llm` sobre el mismo corpus pequeño y registrar precision/recall en el `.md`.
|
||||
|
||||
## Notas
|
||||
|
||||
- Mantener el contrato **identico** al de la version LLM permite usar `deduplicate_entities`, `merge_entity_attributes` y todo el resto del pipeline sin cambios.
|
||||
- GLiNER es malo con IoCs tecnicos (IPs, hashes, wallets) — eso lo cubre 0037. Documentar la limitacion.
|
||||
- El modelo se carga UNA vez por proceso: el caller lo inyecta. No cargarlo dentro de `extract_entities_gliner` — penalty fatal en batch.
|
||||
- Si la GPU no esta disponible, `device="auto"` debe caer a CPU sin error.
|
||||
@@ -0,0 +1,76 @@
|
||||
# 0039 — GLiREL relation extractor (zero-shot relations → triplets)
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0039 |
|
||||
| **Estado** | pendiente |
|
||||
| **Prioridad** | media |
|
||||
| **Tipo** | feature — Python (`python/functions/datascience/`) |
|
||||
|
||||
## Dependencias
|
||||
|
||||
Bloquea-por: **0038** (las entidades vienen del extractor GLiNER o del LLM). Recomendado leer `extract_relations_llm_py_datascience` y `relation_candidate_py_datascience` para mantener contrato.
|
||||
|
||||
**Desbloquea:** 0040 (pipeline hibrido), extraccion de triplets `(sujeto, predicado, objeto)` masiva sin LLM.
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Wrapper sobre GLiREL (`jackboyla/glirel-large-v0`) que, dadas entidades ya extraidas y un set de tipos de relacion, devuelve `list[RelationCandidate]` con el mismo contrato que `extract_relations_llm`. Output natural en triplets `(from, relation_type, to)` reusables por `deduplicate_relations`, `merge_graphs` y `ops_to_sigma_json`.
|
||||
|
||||
## Funciones a crear
|
||||
|
||||
| Function ID | Rol |
|
||||
|---|---|
|
||||
| `glirel_load_model_py_datascience` | Carga + cachea el modelo. `purity: impure` |
|
||||
| `extract_relations_glirel_py_datascience` | Extractor de relaciones. `purity: impure` |
|
||||
|
||||
## Contrato
|
||||
|
||||
```python
|
||||
def glirel_load_model(
|
||||
model_name: str = "jackboyla/glirel-large-v0",
|
||||
device: str = "auto",
|
||||
) -> "GLiREL": ...
|
||||
|
||||
def extract_relations_glirel(
|
||||
text: str,
|
||||
entities: list[EntityCandidate], # del paso anterior (GLiNER o LLM)
|
||||
relation_types: list[str], # ej: ["works_for","owns","communicated_with"]
|
||||
model: "GLiREL",
|
||||
threshold: float = 0.5,
|
||||
max_pairs: int = 0, # 0 = todas las parejas
|
||||
) -> list[RelationCandidate]
|
||||
```
|
||||
|
||||
GLiREL necesita los spans de las entidades (`attributes["start"]`/`end`), por eso 0038 los expone. Si la entidad viene del LLM y no tiene offsets, se buscan con `text.find(name)` como fallback (con warning).
|
||||
|
||||
## Pureza
|
||||
|
||||
Ambas `purity: impure`. `error_type: error_go_core`.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- `python/functions/datascience/glirel_load_model.py` + `.md`
|
||||
- `python/functions/datascience/extract_relations_glirel.py` + `.md`
|
||||
- Tests con 2-3 textos donde las relaciones son explicitas (`X trabaja en Y`, `A llamo a B`), schema de 3-5 relation types.
|
||||
- `pyproject.toml`: añadir `glirel` al extra `nlp`.
|
||||
- Documentar limitacion: GLiREL es bueno para relaciones explicitas en el texto, malo para razonamiento implicito. Para esos casos seguir usando LLM (eso es lo que orquesta 0040).
|
||||
|
||||
## Validacion
|
||||
|
||||
```bash
|
||||
./fn run extract_relations_glirel_py_datascience
|
||||
```
|
||||
|
||||
Comparar precision/recall vs `extract_relations_llm` sobre el mismo corpus + entidades. Registrar en el `.md`.
|
||||
|
||||
## Notas
|
||||
|
||||
- Triplets: el output es `RelationCandidate(from_name, to_name, relation_type, ...)`. `from_name`/`to_name` deben coincidir con entidades del input — si no, descartar (igual que la version LLM).
|
||||
- Si una `relation_type` no aparece en el output, no es un error — solo significa que GLiREL no encontro evidencia.
|
||||
- `deduplicate_relations_py_datascience` ya soporta este formato sin cambios.
|
||||
- Ver issue 0040 para combinar GLiREL con LLM-fallback en el mismo pipeline.
|
||||
@@ -0,0 +1,88 @@
|
||||
# 0040 — Pipeline hibrido extraccion entidades+relaciones (regex + GLiNER/GLiREL + LLM fallback)
|
||||
|
||||
## Metadata
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | 0040 |
|
||||
| **Estado** | pendiente |
|
||||
| **Prioridad** | media |
|
||||
| **Tipo** | feature — Python pipeline (`python/functions/pipelines/`) |
|
||||
|
||||
## Dependencias
|
||||
|
||||
Bloquea-por: **0037** (IoC regex), **0038** (GLiNER), **0039** (GLiREL).
|
||||
|
||||
**Desbloquea:** flujo OSINT/grafo de produccion con coste predecible. Reusable desde apps en `apps/*/` y desde notebooks en `analysis/*/`.
|
||||
|
||||
---
|
||||
|
||||
## Objetivo
|
||||
|
||||
Pipeline que combina los tres extractores en cascada para obtener triplets `(entidad, relacion, entidad)` masivamente con buen coste/calidad:
|
||||
|
||||
1. **Capa regex** (0037) — extrae IoCs tecnicos con precision 100%. Coste 0.
|
||||
2. **Capa GLiNER** (0038) — extrae entidades semanticas (person, organization, location, event...) zero-shot. Coste bajo.
|
||||
3. **Capa GLiREL** (0039) — relaciones zero-shot entre las entidades de capa 1+2.
|
||||
4. **Capa LLM fallback** (existente: `extract_entities_llm` + `extract_relations_llm`) — solo se invoca cuando `confidence < threshold` o sobre los chunks marcados como "complejos".
|
||||
|
||||
Output: `(list[EntityCandidate], list[RelationCandidate])` listos para `deduplicate_entities` → `deduplicate_relations` → `ops_to_sigma_json` / `ops_to_rdf_triples`.
|
||||
|
||||
## Funcion a crear
|
||||
|
||||
| Function ID | Rol |
|
||||
|---|---|
|
||||
| `extract_graph_hybrid_py_pipelines` | Pipeline orquestador. `kind: pipeline`, `purity: impure`, `uses_functions: [...]` con todos los anteriores |
|
||||
|
||||
## Contrato
|
||||
|
||||
```python
|
||||
def extract_graph_hybrid(
|
||||
chunks: list[str],
|
||||
entity_schema: list[dict],
|
||||
relation_types: list[str],
|
||||
gliner_model, # inyectado
|
||||
glirel_model, # inyectado
|
||||
llm_chat_json: Callable | None = None, # opcional; si None, sin fallback LLM
|
||||
ioc_types: list[str] | None = None, # None = todos
|
||||
confidence_threshold: float = 0.6, # bajo este umbral se llama LLM
|
||||
languages: str = "Respond in Spanish.",
|
||||
) -> tuple[list[EntityCandidate], list[RelationCandidate]]
|
||||
```
|
||||
|
||||
Logica interna por chunk:
|
||||
1. `extract_iocs(chunk, ioc_types)` → `EntityCandidate` con `type_ref` tecnico (ip/email/hash/...).
|
||||
2. `extract_entities_gliner(chunk, entity_schema, gliner_model)` → entidades semanticas.
|
||||
3. Si hay chunks con < N entidades o confidence baja **y** hay `llm_chat_json` → `extract_entities_llm` sobre esos chunks; mergear.
|
||||
4. `extract_relations_glirel(chunk, entities_del_chunk, relation_types, glirel_model)`.
|
||||
5. Si baja cobertura de relaciones y hay LLM → `extract_relations_llm` sobre el chunk.
|
||||
6. Devolver listas concatenadas (sin deduplicar — eso lo hace el caller con `deduplicate_*`).
|
||||
|
||||
## Pureza
|
||||
|
||||
`kind: pipeline` → `purity: impure` (regla del registry).
|
||||
`uses_functions`: lista los 5+ extractores invocados. `error_type: error_go_core`.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- `python/functions/pipelines/extract_graph_hybrid.py` + `.md`
|
||||
- Test de integracion en `python/functions/pipelines/tests/test_extract_graph_hybrid.py` con un corpus pequeño (2-3 textos OSINT realistas, mock del LLM).
|
||||
- `.md` documenta: cuando usar fallback LLM, latencia esperada por chunk, recomendacion de batch size.
|
||||
|
||||
## Validacion
|
||||
|
||||
```bash
|
||||
./fn run extract_graph_hybrid_py_pipelines
|
||||
```
|
||||
|
||||
Bench end-to-end sobre 100 KB de texto:
|
||||
- Solo LLM (linea base actual): registrar tiempo y coste estimado.
|
||||
- Pipeline hibrido: registrar tiempo, coste (solo chunks con fallback) y delta de calidad vs solo-LLM.
|
||||
|
||||
Registrar resultados en el `.md` para tener referencia historica.
|
||||
|
||||
## Notas
|
||||
|
||||
- La deduplicacion fuzzy (Levenshtein + Union-Find) ya esta hecha en `deduplicate_entities` — NO replicar aqui.
|
||||
- IoCs y entidades semanticas pueden solapar (ej: GLiNER detecta `apple.com` como organization, regex como domain). Resolver dejando ambas con `type_ref` distinto y que `deduplicate_entities` con `same_type_only=True` no las mezcle. Documentar esta decision.
|
||||
- Pensar en un app `apps/osint_extractor/` que use este pipeline + sigma viz como demo. Fuera de scope de este issue — proponer en proposals despues.
|
||||
@@ -42,3 +42,7 @@
|
||||
| [0034](completed/0034-cpp-scientific-viz.md) | C++ scientific viz (treemap, sankey, chord, contour, voronoi) | completado | media | feature | — |
|
||||
| [0035](0035-cpp-map-tiles.md) | C++ map_tiles (slippy map OSM) | pendiente | baja | feature | — |
|
||||
| [0036](0036-cpp-image-canvas-webcam.md) | C++ image_canvas + webcam_texture | pendiente | baja | feature | — |
|
||||
| [0037](0037-ioc-regex-extractor.md) | IoC regex extractor (IP, email, dominio, hash, wallet, CVE, MAC) | pendiente | alta | feature | — |
|
||||
| [0038](0038-gliner-entity-extractor.md) | GLiNER entity extractor (zero-shot NER multilingue) | pendiente | alta | feature | 0039, 0040 |
|
||||
| [0039](0039-glirel-relation-extractor.md) | GLiREL relation extractor (zero-shot triplets) | pendiente | media | feature | 0040 |
|
||||
| [0040](0040-hybrid-extraction-pipeline.md) | Pipeline hibrido extraccion grafos (regex + GLiNER + GLiREL + LLM fallback) | pendiente | media | feature | — |
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
package infra
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
package infra
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
package infra
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
rtIcon = 3
|
||||
rtGroupIcon = 14
|
||||
|
||||
scnCntInitializedData = 0x00000040
|
||||
scnMemRead = 0x40000000
|
||||
|
||||
dataDirResource = 2
|
||||
resourceTableSlot = 2
|
||||
)
|
||||
|
||||
type icoEntry struct {
|
||||
width uint8
|
||||
height uint8
|
||||
colors uint8
|
||||
reserved uint8
|
||||
planes uint16
|
||||
bitCount uint16
|
||||
size uint32
|
||||
offset uint32
|
||||
data []byte
|
||||
}
|
||||
|
||||
func parseICO(buf []byte) ([]icoEntry, error) {
|
||||
if len(buf) < 6 {
|
||||
return nil, fmt.Errorf("ico too short")
|
||||
}
|
||||
if binary.LittleEndian.Uint16(buf[0:2]) != 0 {
|
||||
return nil, fmt.Errorf("ico reserved field must be 0")
|
||||
}
|
||||
if binary.LittleEndian.Uint16(buf[2:4]) != 1 {
|
||||
return nil, fmt.Errorf("not an icon (type != 1)")
|
||||
}
|
||||
count := int(binary.LittleEndian.Uint16(buf[4:6]))
|
||||
if count == 0 {
|
||||
return nil, fmt.Errorf("ico has 0 images")
|
||||
}
|
||||
if len(buf) < 6+16*count {
|
||||
return nil, fmt.Errorf("ico header truncated")
|
||||
}
|
||||
entries := make([]icoEntry, count)
|
||||
for i := 0; i < count; i++ {
|
||||
off := 6 + 16*i
|
||||
e := icoEntry{
|
||||
width: buf[off],
|
||||
height: buf[off+1],
|
||||
colors: buf[off+2],
|
||||
reserved: buf[off+3],
|
||||
planes: binary.LittleEndian.Uint16(buf[off+4 : off+6]),
|
||||
bitCount: binary.LittleEndian.Uint16(buf[off+6 : off+8]),
|
||||
size: binary.LittleEndian.Uint32(buf[off+8 : off+12]),
|
||||
offset: binary.LittleEndian.Uint32(buf[off+12 : off+16]),
|
||||
}
|
||||
if int(e.offset)+int(e.size) > len(buf) {
|
||||
return nil, fmt.Errorf("ico image %d out of bounds", i)
|
||||
}
|
||||
e.data = buf[e.offset : e.offset+e.size]
|
||||
entries[i] = e
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
type peLayout struct {
|
||||
data []byte
|
||||
peOff int
|
||||
is64 bool
|
||||
numSections int
|
||||
sizeOptHdr int
|
||||
sectionAlign uint32
|
||||
fileAlign uint32
|
||||
sectionsStart int
|
||||
sizeOfImage uint32
|
||||
dataDirCount uint32
|
||||
dataDirOff int
|
||||
checksumOff int
|
||||
sizeOfImageOff int
|
||||
}
|
||||
|
||||
func parsePE(buf []byte) (*peLayout, error) {
|
||||
if len(buf) < 64 || buf[0] != 'M' || buf[1] != 'Z' {
|
||||
return nil, fmt.Errorf("not a PE file (no MZ signature)")
|
||||
}
|
||||
peOff := int(binary.LittleEndian.Uint32(buf[0x3C:0x40]))
|
||||
if peOff+24 > len(buf) {
|
||||
return nil, fmt.Errorf("PE header out of bounds")
|
||||
}
|
||||
if string(buf[peOff:peOff+4]) != "PE\x00\x00" {
|
||||
return nil, fmt.Errorf("not a PE file (no PE signature at e_lfanew)")
|
||||
}
|
||||
coff := peOff + 4
|
||||
numSections := int(binary.LittleEndian.Uint16(buf[coff+2 : coff+4]))
|
||||
sizeOptHdr := int(binary.LittleEndian.Uint16(buf[coff+16 : coff+18]))
|
||||
optOff := coff + 20
|
||||
if optOff+sizeOptHdr > len(buf) {
|
||||
return nil, fmt.Errorf("optional header truncated")
|
||||
}
|
||||
magic := binary.LittleEndian.Uint16(buf[optOff : optOff+2])
|
||||
var is64 bool
|
||||
switch magic {
|
||||
case 0x10B:
|
||||
is64 = false
|
||||
case 0x20B:
|
||||
is64 = true
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown optional header magic 0x%X", magic)
|
||||
}
|
||||
|
||||
var sectionAlignOff, fileAlignOff, sizeOfImageOff, checksumOff, numRvaOff, dataDirOff int
|
||||
if is64 {
|
||||
sectionAlignOff = optOff + 32
|
||||
fileAlignOff = optOff + 36
|
||||
sizeOfImageOff = optOff + 56
|
||||
checksumOff = optOff + 64
|
||||
numRvaOff = optOff + 108
|
||||
dataDirOff = optOff + 112
|
||||
} else {
|
||||
sectionAlignOff = optOff + 32
|
||||
fileAlignOff = optOff + 36
|
||||
sizeOfImageOff = optOff + 56
|
||||
checksumOff = optOff + 64
|
||||
numRvaOff = optOff + 92
|
||||
dataDirOff = optOff + 96
|
||||
}
|
||||
dataDirCount := binary.LittleEndian.Uint32(buf[numRvaOff : numRvaOff+4])
|
||||
|
||||
return &peLayout{
|
||||
data: buf,
|
||||
peOff: peOff,
|
||||
is64: is64,
|
||||
numSections: numSections,
|
||||
sizeOptHdr: sizeOptHdr,
|
||||
sectionAlign: binary.LittleEndian.Uint32(buf[sectionAlignOff : sectionAlignOff+4]),
|
||||
fileAlign: binary.LittleEndian.Uint32(buf[fileAlignOff : fileAlignOff+4]),
|
||||
sectionsStart: optOff + sizeOptHdr,
|
||||
sizeOfImage: binary.LittleEndian.Uint32(buf[sizeOfImageOff : sizeOfImageOff+4]),
|
||||
dataDirCount: dataDirCount,
|
||||
dataDirOff: dataDirOff,
|
||||
checksumOff: checksumOff,
|
||||
sizeOfImageOff: sizeOfImageOff,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type sectionHdr struct {
|
||||
name string
|
||||
virtualSize uint32
|
||||
virtualAddress uint32
|
||||
sizeOfRawData uint32
|
||||
pointerToRawData uint32
|
||||
characteristics uint32
|
||||
headerOff int
|
||||
}
|
||||
|
||||
func (p *peLayout) sections() []sectionHdr {
|
||||
out := make([]sectionHdr, p.numSections)
|
||||
for i := 0; i < p.numSections; i++ {
|
||||
off := p.sectionsStart + 40*i
|
||||
nameBytes := p.data[off : off+8]
|
||||
end := bytes.IndexByte(nameBytes, 0)
|
||||
if end < 0 {
|
||||
end = 8
|
||||
}
|
||||
out[i] = sectionHdr{
|
||||
name: string(nameBytes[:end]),
|
||||
virtualSize: binary.LittleEndian.Uint32(p.data[off+8 : off+12]),
|
||||
virtualAddress: binary.LittleEndian.Uint32(p.data[off+12 : off+16]),
|
||||
sizeOfRawData: binary.LittleEndian.Uint32(p.data[off+16 : off+20]),
|
||||
pointerToRawData: binary.LittleEndian.Uint32(p.data[off+20 : off+24]),
|
||||
characteristics: binary.LittleEndian.Uint32(p.data[off+36 : off+40]),
|
||||
headerOff: off,
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (p *peLayout) hasRsrc() bool {
|
||||
if p.dataDirCount > resourceTableSlot {
|
||||
off := p.dataDirOff + resourceTableSlot*8
|
||||
va := binary.LittleEndian.Uint32(p.data[off : off+4])
|
||||
size := binary.LittleEndian.Uint32(p.data[off+4 : off+8])
|
||||
if va != 0 && size != 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, s := range p.sections() {
|
||||
if s.name == ".rsrc" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func alignUp(v, a uint32) uint32 {
|
||||
if a == 0 {
|
||||
return v
|
||||
}
|
||||
return (v + a - 1) &^ (a - 1)
|
||||
}
|
||||
|
||||
func buildResourceSection(entries []icoEntry, baseRVA uint32) ([]byte, error) {
|
||||
n := uint32(len(entries))
|
||||
|
||||
rootDirSize := uint32(16 + 2*8)
|
||||
rtIconDirSize := uint32(16 + n*8)
|
||||
perIconNameDirSize := uint32(16 + 8)
|
||||
rtGroupDirSize := uint32(16 + 8)
|
||||
groupNameDirSize := uint32(16 + 8)
|
||||
|
||||
leafEntrySize := uint32(16)
|
||||
totalLeafEntries := n + 1
|
||||
leavesSize := leafEntrySize * totalLeafEntries
|
||||
|
||||
dirsSize := rootDirSize + rtIconDirSize + n*perIconNameDirSize + rtGroupDirSize + groupNameDirSize
|
||||
|
||||
dirsAndLeaves := dirsSize + leavesSize
|
||||
|
||||
groupDataSize := uint32(6 + 14*n)
|
||||
dataStartOff := dirsAndLeaves
|
||||
groupDataOff := dataStartOff
|
||||
groupDataPadded := alignUp(groupDataSize, 4)
|
||||
|
||||
iconDataOffsets := make([]uint32, n)
|
||||
cursor := groupDataOff + groupDataPadded
|
||||
for i, e := range entries {
|
||||
iconDataOffsets[i] = cursor
|
||||
cursor += alignUp(uint32(len(e.data)), 4)
|
||||
}
|
||||
totalSize := cursor
|
||||
|
||||
out := make([]byte, totalSize)
|
||||
|
||||
leafOff := dirsSize
|
||||
groupLeafOff := leafOff
|
||||
iconLeavesStart := leafOff + leafEntrySize
|
||||
|
||||
rootOff := uint32(0)
|
||||
rtGroupSubOff := rootDirSize
|
||||
rtIconSubOff := rtGroupSubOff + rtGroupDirSize
|
||||
groupNameSubOff := rtIconSubOff + rtIconDirSize
|
||||
perIconNamesStart := groupNameSubOff + groupNameDirSize
|
||||
|
||||
writeDirHeader := func(off uint32, idEntries uint16) {
|
||||
binary.LittleEndian.PutUint32(out[off:off+4], 0)
|
||||
binary.LittleEndian.PutUint32(out[off+4:off+8], 0)
|
||||
binary.LittleEndian.PutUint16(out[off+8:off+10], 0)
|
||||
binary.LittleEndian.PutUint16(out[off+10:off+12], 0)
|
||||
binary.LittleEndian.PutUint16(out[off+12:off+14], 0)
|
||||
binary.LittleEndian.PutUint16(out[off+14:off+16], idEntries)
|
||||
}
|
||||
writeIDEntry := func(off uint32, id uint32, target uint32, isDir bool) {
|
||||
binary.LittleEndian.PutUint32(out[off:off+4], id)
|
||||
val := target
|
||||
if isDir {
|
||||
val |= 0x80000000
|
||||
}
|
||||
binary.LittleEndian.PutUint32(out[off+4:off+8], val)
|
||||
}
|
||||
|
||||
writeDirHeader(rootOff, 2)
|
||||
writeIDEntry(rootOff+16, rtIcon, rtIconSubOff, true)
|
||||
writeIDEntry(rootOff+24, rtGroupIcon, rtGroupSubOff, true)
|
||||
|
||||
writeDirHeader(rtGroupSubOff, 1)
|
||||
writeIDEntry(rtGroupSubOff+16, 1, groupNameSubOff, true)
|
||||
|
||||
writeDirHeader(groupNameSubOff, 1)
|
||||
writeIDEntry(groupNameSubOff+16, 0, groupLeafOff, false)
|
||||
|
||||
writeDirHeader(rtIconSubOff, uint16(n))
|
||||
for i := uint32(0); i < n; i++ {
|
||||
writeIDEntry(rtIconSubOff+16+i*8, i+1, perIconNamesStart+i*perIconNameDirSize, true)
|
||||
}
|
||||
for i := uint32(0); i < n; i++ {
|
||||
nameDirOff := perIconNamesStart + i*perIconNameDirSize
|
||||
writeDirHeader(nameDirOff, 1)
|
||||
leafForIcon := iconLeavesStart + i*leafEntrySize
|
||||
writeIDEntry(nameDirOff+16, 0, leafForIcon, false)
|
||||
}
|
||||
|
||||
writeLeaf := func(off uint32, dataOff uint32, size uint32) {
|
||||
binary.LittleEndian.PutUint32(out[off:off+4], baseRVA+dataOff)
|
||||
binary.LittleEndian.PutUint32(out[off+4:off+8], size)
|
||||
binary.LittleEndian.PutUint32(out[off+8:off+12], 0)
|
||||
binary.LittleEndian.PutUint32(out[off+12:off+16], 0)
|
||||
}
|
||||
|
||||
writeLeaf(groupLeafOff, groupDataOff, groupDataSize)
|
||||
for i := uint32(0); i < n; i++ {
|
||||
writeLeaf(iconLeavesStart+i*leafEntrySize, iconDataOffsets[i], uint32(len(entries[i].data)))
|
||||
}
|
||||
|
||||
binary.LittleEndian.PutUint16(out[groupDataOff:groupDataOff+2], 0)
|
||||
binary.LittleEndian.PutUint16(out[groupDataOff+2:groupDataOff+4], 1)
|
||||
binary.LittleEndian.PutUint16(out[groupDataOff+4:groupDataOff+6], uint16(n))
|
||||
for i, e := range entries {
|
||||
eo := groupDataOff + 6 + uint32(i)*14
|
||||
out[eo] = e.width
|
||||
out[eo+1] = e.height
|
||||
out[eo+2] = e.colors
|
||||
out[eo+3] = e.reserved
|
||||
binary.LittleEndian.PutUint16(out[eo+4:eo+6], e.planes)
|
||||
binary.LittleEndian.PutUint16(out[eo+6:eo+8], e.bitCount)
|
||||
binary.LittleEndian.PutUint32(out[eo+8:eo+12], e.size)
|
||||
binary.LittleEndian.PutUint16(out[eo+12:eo+14], uint16(i+1))
|
||||
}
|
||||
|
||||
for i, e := range entries {
|
||||
copy(out[iconDataOffsets[i]:], e.data)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SetExeIcon embebe el icono del archivo .ico en el .exe sobreescribiendo
|
||||
// el archivo. Funciona para PE32 y PE32+ que aun no tienen seccion .rsrc
|
||||
// (caso comun de binarios Go compilados sin icono). Si el .exe ya tiene
|
||||
// recursos retorna error.
|
||||
func SetExeIcon(exePath, icoPath string) error {
|
||||
icoBuf, err := os.ReadFile(icoPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read ico: %w", err)
|
||||
}
|
||||
entries, err := parseICO(icoBuf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse ico: %w", err)
|
||||
}
|
||||
|
||||
exeBuf, err := os.ReadFile(exePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read exe: %w", err)
|
||||
}
|
||||
pe, err := parsePE(exeBuf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse pe: %w", err)
|
||||
}
|
||||
if pe.hasRsrc() {
|
||||
return fmt.Errorf("exe already has .rsrc resources; not supported")
|
||||
}
|
||||
if pe.dataDirCount <= resourceTableSlot {
|
||||
return fmt.Errorf("optional header DataDirectory has only %d entries (need >= %d)", pe.dataDirCount, resourceTableSlot+1)
|
||||
}
|
||||
|
||||
sections := pe.sections()
|
||||
if len(sections) == 0 {
|
||||
return fmt.Errorf("exe has no sections")
|
||||
}
|
||||
last := sections[len(sections)-1]
|
||||
|
||||
newSecHdrOff := pe.sectionsStart + 40*pe.numSections
|
||||
if newSecHdrOff+40 > int(last.pointerToRawData) {
|
||||
return fmt.Errorf("not enough space in PE headers for new section header")
|
||||
}
|
||||
|
||||
newRVA := alignUp(last.virtualAddress+last.virtualSize, pe.sectionAlign)
|
||||
newRawOff := alignUp(last.pointerToRawData+last.sizeOfRawData, pe.fileAlign)
|
||||
|
||||
rsrc, err := buildResourceSection(entries, newRVA)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build resource section: %w", err)
|
||||
}
|
||||
rawSize := alignUp(uint32(len(rsrc)), pe.fileAlign)
|
||||
virtSize := uint32(len(rsrc))
|
||||
|
||||
out := make([]byte, 0, int(newRawOff)+int(rawSize))
|
||||
out = append(out, exeBuf[:newRawOff]...)
|
||||
if int(newRawOff) > len(exeBuf) {
|
||||
out = append(out, make([]byte, int(newRawOff)-len(exeBuf))...)
|
||||
}
|
||||
out = append(out, rsrc...)
|
||||
if rawSize > uint32(len(rsrc)) {
|
||||
out = append(out, make([]byte, rawSize-uint32(len(rsrc)))...)
|
||||
}
|
||||
|
||||
hdr := make([]byte, 40)
|
||||
copy(hdr[0:8], ".rsrc\x00\x00\x00")
|
||||
binary.LittleEndian.PutUint32(hdr[8:12], virtSize)
|
||||
binary.LittleEndian.PutUint32(hdr[12:16], newRVA)
|
||||
binary.LittleEndian.PutUint32(hdr[16:20], rawSize)
|
||||
binary.LittleEndian.PutUint32(hdr[20:24], newRawOff)
|
||||
binary.LittleEndian.PutUint32(hdr[36:40], scnCntInitializedData|scnMemRead)
|
||||
copy(out[newSecHdrOff:newSecHdrOff+40], hdr)
|
||||
|
||||
binary.LittleEndian.PutUint16(out[pe.peOff+4+2:pe.peOff+4+4], uint16(pe.numSections+1))
|
||||
|
||||
rsrcEntryOff := pe.dataDirOff + resourceTableSlot*8
|
||||
binary.LittleEndian.PutUint32(out[rsrcEntryOff:rsrcEntryOff+4], newRVA)
|
||||
binary.LittleEndian.PutUint32(out[rsrcEntryOff+4:rsrcEntryOff+8], virtSize)
|
||||
|
||||
newSizeOfImage := alignUp(newRVA+virtSize, pe.sectionAlign)
|
||||
binary.LittleEndian.PutUint32(out[pe.sizeOfImageOff:pe.sizeOfImageOff+4], newSizeOfImage)
|
||||
|
||||
binary.LittleEndian.PutUint32(out[pe.checksumOff:pe.checksumOff+4], 0)
|
||||
|
||||
if err := os.WriteFile(exePath, out, 0o755); err != nil {
|
||||
return fmt.Errorf("write exe: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: set_exe_icon
|
||||
kind: function
|
||||
lang: go
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "func SetExeIcon(exePath, icoPath string) error"
|
||||
description: "Embebe un icono (.ico multi-tamaño) en un ejecutable PE Windows post-build. Implementacion Go pura sin dependencias externas (sin rcedit/wine/rsrc). Parsea el ICONDIR + ICONDIRENTRY del .ico, construye un IMAGE_RESOURCE_DIRECTORY tree con RT_ICON + RT_GROUP_ICON, y appendea una nueva seccion .rsrc al PE. Soporta PE32 y PE32+. No soporta exe que ya tienen recursos (retorna error)."
|
||||
tags: [windows, pe, exe, icon, rcedit, post-build]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [bytes, encoding/binary, fmt, os]
|
||||
params:
|
||||
- name: exePath
|
||||
desc: "ruta absoluta o relativa al .exe Windows a modificar (se sobreescribe in-place)"
|
||||
- name: icoPath
|
||||
desc: "ruta al archivo .ico con uno o mas tamaños de icono"
|
||||
output: "nil si el icono se embebio correctamente; error si el .exe ya tiene recursos, no es PE valido, o el .ico es invalido"
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "functions/infra/set_exe_icon.go"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```go
|
||||
err := infra.SetExeIcon("myapp.exe", "logo.ico")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
## Notas
|
||||
|
||||
- Solo Go binaries cross-compiled a Windows que **no** tengan seccion `.rsrc` previa. La mayoria de binarios Go limpios no la tienen.
|
||||
- Si el .exe ya tiene recursos (creado con `goversioninfo`, `rsrc`, MSVC, etc.), retorna error y no modifica el archivo.
|
||||
- El checksum del PE se pone a 0 tras la modificacion (Windows lo ignora para .exe normales; firmas Authenticode quedarian invalidadas).
|
||||
- Soporta multi-resolucion: si el .ico tiene 16x16, 32x32, 256x256... todos se embeben y Windows elige el mejor.
|
||||
- El icono cambia tras refrescar la cache de iconos de Explorer (a veces requiere `ie4uinit -show` o reiniciar Explorer).
|
||||
@@ -8,7 +8,9 @@ dependencies = [
|
||||
"cryptography>=46.0.6",
|
||||
"fpdf2>=2.8.7",
|
||||
"google-cloud-bigquery>=3.25",
|
||||
"google-cloud-bigquery-datatransfer>=3.22.0",
|
||||
"google-cloud-bigquery-storage>=2.27",
|
||||
"google-cloud-storage>=3.10.1",
|
||||
"httpx",
|
||||
"openpyxl>=3.1.5",
|
||||
"pypdf>=6.10.0",
|
||||
|
||||
Generated
+37
@@ -247,7 +247,9 @@ dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "fpdf2" },
|
||||
{ name = "google-cloud-bigquery" },
|
||||
{ name = "google-cloud-bigquery-datatransfer" },
|
||||
{ name = "google-cloud-bigquery-storage" },
|
||||
{ name = "google-cloud-storage" },
|
||||
{ name = "httpx" },
|
||||
{ name = "openpyxl" },
|
||||
{ name = "pypdf" },
|
||||
@@ -266,7 +268,9 @@ requires-dist = [
|
||||
{ name = "cryptography", specifier = ">=46.0.6" },
|
||||
{ name = "fpdf2", specifier = ">=2.8.7" },
|
||||
{ name = "google-cloud-bigquery", specifier = ">=3.25" },
|
||||
{ name = "google-cloud-bigquery-datatransfer", specifier = ">=3.22.0" },
|
||||
{ name = "google-cloud-bigquery-storage", specifier = ">=2.27" },
|
||||
{ name = "google-cloud-storage", specifier = ">=3.10.1" },
|
||||
{ name = "httpx" },
|
||||
{ name = "openpyxl", specifier = ">=3.1.5" },
|
||||
{ name = "pypdf", specifier = ">=6.10.0" },
|
||||
@@ -386,6 +390,22 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/33/1d3902efadef9194566d499d61507e1f038454e0b55499d2d7f8ab2a4fee/google_cloud_bigquery-3.41.0-py3-none-any.whl", hash = "sha256:2a5b5a737b401cbd824a6e5eac7554100b878668d908e6548836b5d8aaa4dcaa", size = 262343, upload-time = "2026-03-30T22:48:45.444Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-bigquery-datatransfer"
|
||||
version = "3.22.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-api-core", extra = ["grpc"] },
|
||||
{ name = "google-auth" },
|
||||
{ name = "grpcio" },
|
||||
{ name = "proto-plus" },
|
||||
{ name = "protobuf" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0c/e4/a4fa41daf08ad49170b2af1ab1218b964aafeebee47145b647f0a03a6ecc/google_cloud_bigquery_datatransfer-3.22.0.tar.gz", hash = "sha256:9d7778832e9cddba57d2cf1a356497728d24010b4cd24755ed615ac593043e68", size = 112799, upload-time = "2026-03-30T22:51:02.703Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/61/0072e0a947b5afa4d10c047daeea0b21922beb17ee1c3d14bb3c3ba49f7c/google_cloud_bigquery_datatransfer-3.22.0-py3-none-any.whl", hash = "sha256:4ca1c9f65ec397d2cce1e79adb78fdbd09810e9e854f0ec300a45336a8b623f3", size = 89343, upload-time = "2026-03-30T22:49:25.662Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-bigquery-storage"
|
||||
version = "2.37.0"
|
||||
@@ -415,6 +435,23 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/73/d9/5bb050cb32826466aa9b25f79e2ca2879fe66cb76782d4ed798dd7506151/google_cloud_core-2.5.1-py3-none-any.whl", hash = "sha256:ea62cdf502c20e3e14be8a32c05ed02113d7bef454e40ff3fab6fe1ec9f1f4e7", size = 29452, upload-time = "2026-03-30T22:48:31.567Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-storage"
|
||||
version = "3.10.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "google-api-core" },
|
||||
{ name = "google-auth" },
|
||||
{ name = "google-cloud-core" },
|
||||
{ name = "google-crc32c" },
|
||||
{ name = "google-resumable-media" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/ff/ca9ab2417fa913d75aae38bf40bf856bb2749a604b2e0f701b37cfcd23cc/google_cloud_storage-3.10.1-py3-none-any.whl", hash = "sha256:a72f656759b7b99bda700f901adcb3425a828d4a29f911bc26b3ea79c5b1217f", size = 324453, upload-time = "2026-03-23T09:35:21.368Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-crc32c"
|
||||
version = "1.8.0"
|
||||
|
||||
Reference in New Issue
Block a user