parse_recommendation_payload() (añadida en v2.4.0) invoca
directamente db.execute_query() sin pasar por sanitize_input().
La arista parse_recommendation_payload → execute_query no existía antes.
parse_recommendation_payload es ahora parte del subgrafo tainted
y hereda el blast radius de execute_query (52 nodos downstream).
/api/v2/recommendations es
untrusted_external y su nueva arista al store salta el boundary
de privilegio api → db.
sanitize_input() antes de execute_query() en parse_recommendation_payload.parse_recommendation_payload (CC=14, candidato genotoxic).validate_pet_profile() eliminó la llamada a
check_ownership(user_id, pet_id). La arista
validate_pet_profile → check_ownership desapareció del grafo.
check_ownership estaba anotada como privilege_boundary;
su desaparición del call graph levanta la barrera de autorización.
api → ownership_check no quedó reflejada en el diff
de texto porque la función validate_pet_profile sigue existiendo.
check_ownership() dentro de validate_pet_profile() o su caller directo.process_ai_request en sub-funciones (CC objetivo < 10).AuthService (no acceso directo desde workers).| Nodo | Tipo | Trust Level | Archivo | Delta |
|---|---|---|---|---|
| POST /api/v2/recommendations | REST endpoint | untrusted_external | api/v2/recommendations.py:48 | NUEVO |
| POST /api/v2/batch-recommendations | REST endpoint | untrusted_external | api/v2/batch.py:22 | NUEVO |
| celery.task: ai_recommendation_task | Async task | untrusted_external | workers/ai_worker.py:15 | NUEVO |
| GET /api/v1/pets/profile (legacy) | REST endpoint | trusted_internal | api/v1/pets.py:31 | ELIMINADO |
| Nodo | Tipo | Tainted vía | Archivo |
|---|---|---|---|
| parse_recommendation_payload | function | POST /api/v2/recommendations → parse_recommendation_payload | api/v2/recommendations.py:72 |
| process_ai_request | function | celery.task: ai_recommendation_task → process_ai_request | workers/ai_worker.py:54 |
| build_prompt | function | process_ai_request → build_prompt | workers/ai_worker.py:89 |
| validate_pet_profile | function | POST /api/v2/recommendations → validate_pet_profile | domain/pets.py:103 |
| fetch_ai_credentials | function | process_ai_request → fetch_ai_credentials | workers/ai_worker.py:112 |
| Nodo | Tipo | Downstream antes | Downstream después | Delta | Archivo |
|---|---|---|---|---|---|
| process_ai_request | function | 8 | 31 | +23 | workers/ai_worker.py:54 |
| AIRecommendationService | class | 0 | 18 | +18 (nuevo) | services/ai_service.py:12 |
| validate_pet_profile | function | 5 | 14 | +9 | domain/pets.py:103 |
| LegacyProfileSerializer | class | 12 | 3 | -9 | serializers/legacy.py:7 |
| Nodo | CC antes | CC después | Delta | En tainted | Archivo |
|---|---|---|---|---|---|
| process_ai_request | 6 | 18 | +12 | ✓ | workers/ai_worker.py:54 |
| validate_pet_profile | 4 | 9 | +5 | ✓ | domain/pets.py:103 |
| parse_recommendation_payload | — | 14 | +14 (nuevo) | ✓ | api/v2/recommendations.py:72 |
| build_prompt | 2 | 7 | +5 | ✓ | workers/ai_worker.py:89 |
| NutritionCalculator.compute | 7 | 9 | +2 | ✗ | domain/nutrition.py:45 |
| Nodo | Tipo | CC | Entrypoint reachable | Archivo |
|---|---|---|---|---|
| AIRecommendationService | class | — | ✓ | services/ai_service.py:12 |
| parse_recommendation_payload | function | 14 | ✓ | api/v2/recommendations.py:72 |
| process_batch | function | 11 | ✓ | api/v2/batch.py:55 |
| fetch_ai_credentials | function | 5 | ✓ | workers/ai_worker.py:112 |
| HealthCheckSerializer | class | — | ✗ | serializers/health.py:3 |
| Nodo | Tipo | CC | Era privilege_boundary | Archivo (before) |
|---|---|---|---|---|
| check_ownership | function | 3 | ✓ ← REVISIÓN REQUERIDA | domain/authorization.py:88 |
| LegacyProfileSerializer | class | — | ✗ | serializers/legacy.py:7 |
| v1_recommendations_handler | function | 5 | ✗ | api/v1/recommendations.py:14 |
trailmark diff --json (nodos/aristas/entrypoints) + graph_diff.py (subgraph membership).git diff v2.3.1..v2.4.0 para anotaciones de evidencia.