Tres repos públicos, un pipeline: qué salió (y qué no)
Corremos NP Auditor sobre aws-cli (AWS), Django (DSF) y Metaflow (Netflix): clones shallow, scans acotados, sin ejecutar el target y sin tocar el banco de firmas. Aquí va el resultado con números reales — para mortales y para quien quiera el detalle técnico.
Esto no es “hackeamos AWS / Django / Netflix”. Es una pasada SAST + cuarentena heurística sobre subdirectorios públicos. Hay señal útil y también ruido. Lo honesto es mostrar ambos.
Método: qué es Fase E
Fase E es el experimento de Capa2 sobre código real (no solo fixtures). El pipeline va en tres capas de lectura:
obj_cmd_injection, obj_pickle_rce, etc.). Sale score + familia — pendiente de revisión humana / oráculo.
Reglas del run (sin atajos)
- No exec del código del target — solo lectura AST / firmas.
- No escribir
BANCO_FIRMAS— el LLM / pipeline no inventa firmas nuevas en el banco. - R1: en staging,
cwe_non_null = 0(sin CWE inventado en cuarentena). - Oráculo solo fixtures: en código real,
oraculo_pass = 0es esperado — el oráculo no “aprueba” repos ajenos. - Shallow clone (
--depth 1), scans por subdir (no monorepo completo).
Runner: run_capa2_fase_e_v2.py · ts 2026-08-01T21:48:39Z ·
cerebro aislado en repos_reales/ para no pisar fixtures.
Números agregados
| Repo | Org | Capa1 | Cuarentena | Match obj | Nuevos | .py ≈ | s |
|---|---|---|---|---|---|---|---|
| aws-cli | AWS | 14 | 138 | 99 | 39 | 219 | 26.4 |
| django | DSF | 23 | 462 | 400 | 62 | 319 | 8.5 |
| metaflow | Netflix | 68 | 319 | 228 | 91 | 418 | 9.7 |
| Total | 105 | 919 | 727 | 192 | 956 | 44.6 |
Top objetivos globales (match): obj_sqli_concat 192 ·
obj_rce_eval 188 · obj_mass_assignment 158 ·
obj_cmd_injection 91 · obj_ssti 29.
Una parte de eso es ruido de familia (ver más abajo).
Si las tablas se sienten frías: en cada blog por repo hay auditoría con cerebro —
impacto, lo no etiquetado (candidato_nuevo), ruido vs señal y veredicto P0/P1/P2 de investigación
(threat-model defensivo, sin PoCs). Empieza por
aws-cli,
Django o
Metaflow.
Y si quieres el lado producto — Claude en el IDE leyendo el pack, no el motor —
está el caso Claude + Fase E.
aws-cli — Amazon Web Services
Auditoría completa de aws-cli →
Miramos el paquete awscli/ (≈219 archivos Python) en un clone shallow de
develop (4b39e80…).
Capa1 marcó 14 patrones conocidos (rutas, logs, plantillas).
La cuarentena metió 138 sinks; 99 pegaron a un objetivo
(sobre todo command injection y “eval-like”) y 39 quedaron como candidatos nuevos.
Significa: el motor encuentra lugares donde el CLI habla con el shell, abre zips o arma rutas —
candidatos a revisión. No significa: que AWS tenga 99 bugs explotables listos para CVE.
| Métrica | Valor |
|---|---|
| URL / branch / SHA | aws/aws-cli · develop · 4b39e80d |
| Scan path | awscli/ (sin tests/ completo como foco) |
| Capa1 conocidos | 14 |
| Cuarentena / match / nuevos | 138 / 99 / 39 |
| Oráculo pass/fail | 0 / 0 |
| Top objetivos | cmd_injection 32 · rce_eval 27 · mass_assignment 16 |
| CWE Capa1 (top) | CWE-117 ×5 · CWE-22 ×4 · CWE-1336 ×1 |
Detalle técnico — ejemplos del scan
cloudformation/artifact_exporter.py:448 y helpers de testutils.py. Path traversal flag — revisar contención real del path.# piping to a pager to work, we need to use shell=True. kwargs['shell'] = True return Popen(*args, **kwargs)
alias.py:293 (compat_shell_quote(..., shell=True)) e invoker con shell=True. Señal real de superficie shell — contexto de pager/alias, no “proof of exploit”.with closing(zipfile.ZipFile(fileobj)) as f:
f.infolist()
setattr → familia mass_assignment es heurística floja en CLIs (namespaces de argparse). Ejemplo claro de match semántico ruidoso.Django — Django Software Foundation
Auditoría completa de Django →
Escaneamos subdirs del core: core, db, http, utils,
views, forms (≈319 .py) en main (60121939…).
Capa1 encontró 23 hits conocidos — el más llamativo: pickle en backends de cache (CWE-502).
La cuarentena explotó a 462 tracks; 400 hicieron match (muchísimo
obj_sqli_concat por cursor.execute parametrizado — ruido).
Significa: el framework usa patrones que un SAST tiene que ver (pickle, SQL helpers, templates).
No significa: que Django esté “roto” en prod; varios hits son diseño documentado o FP de familia.
| Métrica | Valor |
|---|---|
| URL / branch / SHA | django/django · main · 60121939 |
| Scan paths | core · db · http · utils · views · forms |
| Capa1 conocidos | 23 |
| Cuarentena / match / nuevos | 462 / 400 / 62 |
| Oráculo pass/fail | 0 / 0 |
| Top objetivos | sqli_concat 180 · mass_assignment 87 · rce_eval 85 |
| CWE Capa1 (top) | CWE-502 ×6 · CWE-347 ×5 · CWE-829 ×2 · CWE-89 ×2 |
Detalle técnico — ejemplos del scan
if not self._is_expired(f):
return pickle.loads(zlib.decompress(f.read()))
locmem.py, redis.py y en cuarentena
db.py:102 (pickle.loads(base64…) → obj_pickle_rce 0.9).
Pickle en cache es un patrón clásico de Django (confianza en el store). Señal correcta del motor; exploitabilidad depende de quién escribe el cache.
with connection.cursor() as cursor:
cursor.execute(
"SELECT %s …" # placeholders — no concat de user input crudo
obj_sqli_concat en Django: muchos son cursor.execute parametrizado.
El sink existe; la familia “concat” se pasa de lanza. Caso de estudio del FP semántico.
"git log --pretty=format:%ct --quiet -1 HEAD",
capture_output=True,
shell=True,
compiler.compile del SQL compiler de Django — no es
compile() de bytecode Python. La familia obj_rce_eval se confunde con el nombre del método.
Metaflow — Netflix
Auditoría completa de Metaflow →
Paquete metaflow/ (≈418 .py) en master (3d6fa453…).
Aquí Capa1 fue el más cargado: 68 conocidos (pickle, paths, HTML, eval, permisos…).
Cuarentena 319 · match 228 · nuevos 91.
Aparece mucho subprocess / shell=True (bootstraps de pypi/uv y vendor click).
Significa: un orquestador de ML toca shell, serialización y vendors — superficie grande, el scanner la ve.
No significa: 68 CVEs nuevas; varios hits viven en _vendor/ o son diseño del runner.
| Métrica | Valor |
|---|---|
| URL / branch / SHA | Netflix/metaflow · master · 3d6fa453 |
| Scan path | metaflow/ |
| Capa1 conocidos | 68 |
| Cuarentena / match / nuevos | 319 / 228 / 91 |
| Oráculo pass/fail | 0 / 0 |
| Top objetivos | rce_eval 76 · mass_assignment 55 · cmd_injection 50 |
| CWE Capa1 (top) | CWE-502 ×11 · CWE-916 ×8 · CWE-347 ×7 · CWE-22 ×6 · CWE-79 ×5 |
Detalle técnico — ejemplos del scan
env_escape/data_transferer.py (varios sitios).
Misma lógica que Django cache: deserialización explícita — el motor la etiqueta bien; el riesgo depende del trust boundary.
_parser.py, markers.py, version.py…).
Tratar con escepticismo: firma HTML sobre librería de versiones — candidato a FP de firma, no XSS web clásico.
if subprocess.call(cmdline, shell=True):
raise ExternalCommandFailed(
"Command '%s' returned a non-zero exit code." % cmdline)
plugins/pypi/bootstrap.py y plugins/uv/bootstrap.py con
subprocess.run(..., shell=True), y vendor click
(_termui_impl.py con os.system / Popen(..., shell=True)).
Ruido, oráculo y límites
match_objetivo = 727 no es “727 bugs confirmados”.
Es “727 sinks que la heurística cruzó con una familia de objetivo”.
El oráculo (oraculo_pass = 0) solo corre sobre fixtures propias — en código real queda
pendiente revisión. descartado_fp = 0 en estos runs: el filtro FP mínimo casi no aplicó.
Ruido que vimos en vivo
obj_sqli_concatsobrecursor.executeparametrizado (Django cache DB).setattr→obj_mass_assignmenten argparse / color / winconsole.compiler.compile(SQL) →obj_rce_evalpor homonimia.re.compile→ a veces cae en familias eval/redos sin ser RCE.- CWE-79 en packaging vendorizado (Metaflow) — firma HTML dudosa en ese contexto.
Limitaciones del experimento
- Scans acotados: no es el monorepo completo (aws-cli sin foco total en tests; Django sin contrib/tests).
- Shallow clone: un commit, sin historia.
- No se encadenó promoción a
BANCO_FIRMASni staging_firmas. - Verificación R1 OK: staging sin CWE inventado; sin exec del target.
Conclusión (honesta)
El pipeline sí produce señal en código real grande: pickle en cache Django,
shell=True / Popen en aws-cli y Metaflow, ZipFile en caminos de empaquetado, paths y logs en Capa1.
Eso es exactamente lo que quieres de un SAST + cuarentena: superficie visible, con archivo y línea.
Y también produce ruido heurístico: familias que se pasan de lanza, vendors, nombres de método engañosos. Por eso Fase E deja los matches en cuarentena y no escribe el banco — Store → analyze; humano (o fixtures) decide.
Si te llevas una frase: corrimos el motor sobre tres repos que la gente sí usa, en ~45 segundos, sin ejecutar su código, y salió un mapa útil de sinks — no un trophy wall de “hackeamos AWS”.
Agregado: fase_e_repos_reales_latest.json · por repo bajo
cerebro/capa2_objetivos/repos_reales/{aws-cli,django,metaflow}/ ·
tmp: /tmp/np-fase-e-repos/reporte_agregado.json.
Desglose por repo: aws-cli · Django · Metaflow. Cómo Claude revisa esos packs en el IDE: caso Claude + Fase E. ¿Quieres el producto, no solo el caso? Vuelve al home o lee cómo jala NP Auditor.