Add SECURITY_TRIAGE.md for vulnerability assessment and remediation plan

- Document current state of vulnerabilities in Rust and frontend dependencies
- Outline active vulnerabilities and their remediation status
- Include notes on resolved issues and remaining bugs
- Define a remediation plan with prioritized actions
This commit is contained in:
2026-04-28 15:47:20 -04:00
parent 2c8bfaa20e
commit 42620cc9ac
42 changed files with 2032 additions and 1869 deletions
@@ -92,7 +92,7 @@ async fn connect_mysql_pool(env_var: &str) -> Result<sqlx::MySqlPool, (StatusCod
use sqlx::mysql::MySqlPoolOptions;
let mysql_url = std::env::var(env_var)
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, format!("{} no configurada", env_var)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
let mut last_error = String::new();
@@ -379,7 +379,7 @@ pub async fn create_question(
.bind(media_type.as_deref())
.fetch_one(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
Ok(Json(question))
}
@@ -501,7 +501,7 @@ pub async fn list_questions(
.fetch_all(&pool)
.await
}
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
Ok(Json(questions))
}
@@ -627,7 +627,7 @@ pub async fn delete_question(
.bind(org_ctx.id)
.execute(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
if result.rows_affected() == 0 {
return Err((StatusCode::NOT_FOUND, "Pregunta no encontrada".to_string()));
@@ -690,7 +690,7 @@ pub async fn import_from_mysql(
)
.fetch_all(&mysql_pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
tracing::info!("Se obtuvieron {} cursos de MySQL", mysql_courses.len());
@@ -698,7 +698,7 @@ pub async fn import_from_mysql(
tracing::info!("Guardando planes y cursos en PostgreSQL...");
save_mysql_courses_and_plans(&pool, org_ctx.id, mysql_plans, mysql_courses)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al guardar cursos/planes: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
// Obtener preguntas de MySQL
let mysql_questions: Vec<MySqlQuestion> = if payload.import_all.unwrap_or(false) {
@@ -718,7 +718,7 @@ pub async fn import_from_mysql(
)
.fetch_all(&mysql_pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
} else if let Some(course_id) = payload.mysql_course_id {
// Importar todas las preguntas para un curso específico (sin límite)
sqlx::query_as(
@@ -737,7 +737,7 @@ pub async fn import_from_mysql(
.bind(course_id)
.fetch_all(&mysql_pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?
} else if let Some(question_ids) = payload.question_ids {
// Obtener IDs de preguntas específicas - usar enfoque simple
let mut imported_questions: Vec<QuestionBank> = vec![];
@@ -758,7 +758,7 @@ pub async fn import_from_mysql(
.bind(q_id)
.fetch_optional(&mysql_pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener la pregunta {}: {}", q_id, e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
if let Some(question) = mq {
// Mapear el tipo de pregunta de MySQL al tipo de pregunta de la plataforma
@@ -806,7 +806,7 @@ pub async fn import_from_mysql(
.bind(&source_metadata)
.fetch_one(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al importar la pregunta {}: {}", question.id_pregunta, e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
imported_questions.push(qb);
}
@@ -837,7 +837,7 @@ pub async fn import_from_mysql(
.bind(org_ctx.id)
.fetch_one(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al comprobar existencia: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
if exists.0 {
skipped_count += 1;
@@ -893,7 +893,7 @@ pub async fn import_from_mysql(
.bind(mq.id_cursos)
.fetch_one(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al importar la pregunta {}: {}", mq.id_pregunta, e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
imported_questions.push(question);
}
@@ -1041,7 +1041,7 @@ pub async fn list_mysql_courses(
)
.fetch_all(&mysql_pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
mysql_pool.close().await;
@@ -1067,7 +1067,7 @@ pub async fn get_mysql_plans(
.bind(org_ctx.id)
.fetch_all(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los planes: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
// Respaldo compatible con versiones anteriores: si el reflejo SAM está vacío, usar el reflejo de metadatos heredado.
if plans.is_empty() {
@@ -1084,7 +1084,7 @@ pub async fn get_mysql_plans(
.bind(org_ctx.id)
.fetch_all(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los planes heredados: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
}
// Auto-sincronización de último recurso: si sigue vacío, obtener metadatos de MySQL y persistirlos.
@@ -1208,7 +1208,7 @@ pub async fn get_mysql_courses_by_plan(
.bind(filters.plan_id)
.fetch_all(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
// Intentar refrescar desde MySQL (fuente SAM) para evitar listas incompletas por espejo desactualizado.
if let Ok(mysql_pool) = connect_mysql_pool("MYSQL_DATABASE_URL").await {
@@ -1315,7 +1315,7 @@ pub async fn get_mysql_courses_by_plan(
.bind(filters.plan_id)
.fetch_all(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos heredados: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
}
Ok(Json(courses))
@@ -1408,7 +1408,7 @@ pub async fn import_all_from_mysql(
)
.fetch_all(&mysql_pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener los cursos: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
tracing::info!("Se obtuvieron {} cursos de MySQL", mysql_courses.len());
@@ -1416,7 +1416,7 @@ pub async fn import_all_from_mysql(
tracing::info!("Guardando planes y cursos en PostgreSQL...");
save_mysql_courses_and_plans(&pool, org_ctx.id, mysql_plans.clone(), mysql_courses.clone())
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al guardar cursos/planes: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
// Si solo se solicita la importación de metadatos, salir antes
if import_metadata_only {
@@ -1477,7 +1477,7 @@ pub async fn import_all_from_mysql(
)
.fetch_all(&mysql_pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener las preguntas: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
mysql_pool.close().await;
@@ -1505,7 +1505,7 @@ pub async fn import_all_from_mysql(
.bind(org_ctx.id)
.fetch_optional(&pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Fallo en la comprobación: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
match existing {
Some((id, is_active)) => {
@@ -1724,14 +1724,14 @@ pub async fn ai_generate_question(
}))
.send()
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("La solicitud de IA falló: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
if !response.status().is_success() {
return Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Error de la API de IA: {}", response.status())));
return Err((StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()));
}
let result: serde_json::Value = response.json().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al analizar la respuesta de la IA: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
// Extraer contenido de la respuesta de Ollama
let content = result
@@ -1742,7 +1742,7 @@ pub async fn ai_generate_question(
// Analizar la respuesta de la IA como JSON
let ai_question: AIQuestionResponse = serde_json::from_str(content)
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al analizar el JSON de la pregunta: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
Ok(Json(ai_question))
}
@@ -1801,7 +1801,7 @@ pub async fn import_course_from_mysql(
if let sqlx::Error::RowNotFound = e {
(StatusCode::NOT_FOUND, format!("Curso con ID {} no encontrado en MySQL", payload.mysql_course_id))
} else {
(StatusCode::INTERNAL_SERVER_ERROR, format!("Error al obtener el curso de MySQL: {}", e))
(StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string())
}
})?;
@@ -1809,7 +1809,7 @@ pub async fn import_course_from_mysql(
// Iniciar transacción
let mut tx = pool.begin().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al iniciar la transacción: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
// Determinar el tipo y nivel del curso para la generación de la estructura
let course_type = calculate_course_type_from_duration(mysql_course.duracion);
@@ -1845,7 +1845,7 @@ pub async fn import_course_from_mysql(
.bind(mysql_course.id_cursos)
.fetch_one(&mut *tx)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al crear el curso: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
tracing::info!("Curso creado en PostgreSQL: {}", new_course.id);
@@ -1863,7 +1863,7 @@ pub async fn import_course_from_mysql(
// Confirmar transacción
tx.commit().await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Error al confirmar la transacción: {}", e)))?;
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string()))?;
tracing::info!(
"Curso {} importado con éxito con {} módulos y {} lecciones",