diff --git a/dev/issues/0037-ioc-regex-extractor.md b/dev/issues/0037-ioc-regex-extractor.md new file mode 100644 index 00000000..c0317ea7 --- /dev/null +++ b/dev/issues/0037-ioc-regex-extractor.md @@ -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_(text: str) -> list[dict]: + """ + Returns: + [{"value": str, "start": int, "end": int, "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. diff --git a/dev/issues/0038-gliner-entity-extractor.md b/dev/issues/0038-gliner-entity-extractor.md new file mode 100644 index 00000000..16fe0880 --- /dev/null +++ b/dev/issues/0038-gliner-entity-extractor.md @@ -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. diff --git a/dev/issues/0039-glirel-relation-extractor.md b/dev/issues/0039-glirel-relation-extractor.md new file mode 100644 index 00000000..2d8ac2e5 --- /dev/null +++ b/dev/issues/0039-glirel-relation-extractor.md @@ -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. diff --git a/dev/issues/0040-hybrid-extraction-pipeline.md b/dev/issues/0040-hybrid-extraction-pipeline.md new file mode 100644 index 00000000..b5c9aa08 --- /dev/null +++ b/dev/issues/0040-hybrid-extraction-pipeline.md @@ -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. diff --git a/dev/issues/README.md b/dev/issues/README.md index 39ff9dfd..bece9399 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -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 | — |