Workflow de Aumentación
Paso 1 — Grafo construido + Pre-análisis
engine.preanalysis() completado · 47 nodos · taint tracking activo · blast radius calculado
Paso 2 — Archivos SARIF y weAudit localizados
results_semgrep.sarif · results_codeql.sarif · .vscode/alice.weaudit
Paso 3 — Aumentación ejecutada
matched_findings: 31 · unmatched_findings: 3 (fuera de scope) · subgraphs_created: 12
Paso 4 — Hallazgos y subgrafos inspeccionados
engine.findings() devuelve 31 nodos anotados · 12 subgrafos disponibles
Paso 5 — Cruce con pre-análisis
sarif:error ∩ tainted = 5 nodos críticos · sarif:error ∩ high_blast_radius = 4 nodos · 2 privilege_boundary comprometidos
Salida de Augmentation Engine
CLI Output
# Paso 1: Pre-análisis
$ uv run trailmark preanalysis api/ --language python
→ 47 nodes parsed · taint sources: 8 · blast radius computed
# Paso 3: Aumentación con ambas fuentes
$ uv run trailmark augment api/ \
--sarif results_semgrep.sarif --sarif results_codeql.sarif \
--weaudit .vscode/alice.weaudit --json
{
"matched_findings": 31,
"unmatched_findings": 3,
"subgraphs_created": [
"sarif:error", "sarif:warning", "sarif:note",
"sarif:semgrep", "sarif:codeql",
"weaudit:high", "weaudit:medium", "weaudit:low",
"weaudit:findings", "weaudit:notes",
"weaudit:alice_garcia"
],
"annotations_added": 31,
"taint_overlap": 14,
"blast_radius_overlap": 9
}
$ uv run trailmark preanalysis api/ --language python
→ 47 nodes parsed · taint sources: 8 · blast radius computed
# Paso 3: Aumentación con ambas fuentes
$ uv run trailmark augment api/ \
--sarif results_semgrep.sarif --sarif results_codeql.sarif \
--weaudit .vscode/alice.weaudit --json
{
"matched_findings": 31,
"unmatched_findings": 3,
"subgraphs_created": [
"sarif:error", "sarif:warning", "sarif:note",
"sarif:semgrep", "sarif:codeql",
"weaudit:high", "weaudit:medium", "weaudit:low",
"weaudit:findings", "weaudit:notes",
"weaudit:alice_garcia"
],
"annotations_added": 31,
"taint_overlap": 14,
"blast_radius_overlap": 9
}
Hallazgos Prioritarios — sarif:error ∩ tainted ∩ high_blast_radius
SQL Injection via parámetro no sanitizado
La función
execute_raw_query() construye queries SQL concatenando directamente request.query_params["filter"] sin parameterización. Un atacante puede extraer o modificar cualquier tabla de la BD.📄 api/routes/query.py · líneas 45–67
☣ Tainted — flujo desde HTTP input
💥 Blast radius: ALTO (23 nodos alcanzables)
🔒 Privilege boundary cruzado
Server-Side Request Forgery (SSRF) en exportación
export_to_url() realiza requests.post(user_provided_url, data=payload) sin validar el esquema ni el rango IP de destino. Confirmado por auditora humana: permite exfiltrar metadatos de AWS EC2 (169.254.169.254).📄 api/routes/export.py · líneas 23–41
☣ Tainted — URL desde body JSON
💥 Blast radius: ALTO (18 nodos)
Deserialización insegura con pickle en caché Redis
cache.get_cached() usa pickle.loads() sobre datos de Redis sin verificar firma. Si un atacante tiene acceso a Redis (o a la red interna), puede ejecutar código arbitrario en el servidor.📄 api/utils/cache.py · líneas 12–28
☣ Tainted — Redis data sin firmar
💥 Blast radius: MUY ALTO (31 nodos)
🔒 Privilege boundary
Clave JWT hardcodeada en código fuente
jwt_handler.py usa SECRET_KEY = "dataflow-secret-2024" hardcodeado. Cualquier persona con acceso al repo puede forjar tokens de autenticación válidos para cualquier usuario.📄 api/auth/jwt_handler.py · líneas 8–10
🔒 Privilege boundary — autenticación
💥 Blast radius: ALTO (todos los usuarios)
Mass Assignment en modelo de usuario
El endpoint de actualización de perfil pasa el body JSON directamente a
User(**request.json()) sin lista blanca de campos, permitiendo que un usuario normal eleve su propio role a "admin".📄 api/models/user.py · líneas 54–72
☣ Tainted — JSON body sin filtrar
🔒 Privilege boundary escalación
✓ Solo hallazgo humano — SAST no detectó
Grafo de Código — Subgrafo sarif:error ∩ tainted
Trailmark Visualization
sarif:error
tainted
high_blast_radius
privilege_boundary
weaudit:alice_garcia
nodo limpio
📊 Cobertura por Archivo
| Archivo | Hallazgos | Riesgo |
|---|---|---|
| api/routes/query.py | 5 critical |
CRÍTICO
|
| api/utils/cache.py | 4 critical |
CRÍTICO
|
| api/routes/export.py | 3 high |
ALTO
|
| api/auth/jwt_handler.py | 2 high |
ALTO
|
| api/models/user.py | 3 medium |
MEDIO
|
🎯 Matriz de Triaje — Cruce pre-análisis
Hallazgos en intersección de sarif:error ∩ tainted ∩ high_blast_radius
execute_raw_query — SQLi + taint + blast + priv
P0
get_cached (pickle.loads) — taint + blast máximo
P0
export_to_url — SSRF + taint externo
P0
jwt_handler — hardcoded secret + priv boundary
P1
User mass assignment — taint JSON body
P1
Logging de tokens en request.py (warning)
P2
Ausencia de rate limiting en /query
P3
Anotaciones en Grafo — Formato Trailmark
# engine.annotations_of("execute_raw_query")
[
{
"kind": "finding",
"source": "sarif:semgrep",
"description": "[ERROR] python.lang.security.sqli.raw-query-format-string: Raw SQL format string (Semgrep)",
"location": { "file": "api/routes/query.py", "line_start": 45, "line_end": 67 }
},
{
"kind": "finding",
"source": "sarif:codeql",
"description": "[ERROR] CWE-89: SQL query built from user-controlled source (CodeQL)",
"location": { "file": "api/routes/query.py", "line_start": 52, "line_end": 61 }
}
]
# Subgraph overlap — sarif:error ∩ tainted:
→ 5 nodos comunes: execute_raw_query, get_cached, export_to_url,
build_filter_clause, deserialize_redis_value
[
{
"kind": "finding",
"source": "sarif:semgrep",
"description": "[ERROR] python.lang.security.sqli.raw-query-format-string: Raw SQL format string (Semgrep)",
"location": { "file": "api/routes/query.py", "line_start": 45, "line_end": 67 }
},
{
"kind": "finding",
"source": "sarif:codeql",
"description": "[ERROR] CWE-89: SQL query built from user-controlled source (CodeQL)",
"location": { "file": "api/routes/query.py", "line_start": 52, "line_end": 61 }
}
]
# Subgraph overlap — sarif:error ∩ tainted:
→ 5 nodos comunes: execute_raw_query, get_cached, export_to_url,
build_filter_clause, deserialize_redis_value