Manejo de Errores - mcp-scan¶
Esta guia describe los tipos de errores que puede generar mcp-scan y como manejarlos correctamente.
Indice¶
- Tipos de Errores
- Errores del Scanner
- Errores de Contexto
- Errores de Baseline
- Errores de Reporte
- Patrones de Manejo
- Logging y Diagnostico
Tipos de Errores¶
mcp-scan puede producir diferentes categorias de errores:
| Categoria | Descripcion | Recuperable |
|---|---|---|
| Descubrimiento | Errores al buscar archivos | Parcialmente |
| Parsing | Errores al parsear codigo fuente | Si (archivo ignorado) |
| Analisis | Errores durante el analisis | Si (archivo ignorado) |
| Contexto | Timeout o cancelacion | No |
| Baseline | Errores al cargar/guardar baseline | Si |
| Reporte | Errores al generar reportes | Si |
| Configuracion | Configuracion invalida | No |
Errores del Scanner¶
Error de Descubrimiento¶
Ocurre cuando no se puede acceder al directorio o archivo especificado.
package main
import (
"context"
"errors"
"fmt"
"os"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
s := scanner.New(scanner.DefaultConfig())
result, err := s.Scan(context.Background(), "/ruta/inexistente")
if err != nil {
// Verificar si es error de archivo no encontrado
if os.IsNotExist(errors.Unwrap(err)) {
fmt.Println("Error: La ruta especificada no existe")
os.Exit(2)
}
// Verificar si es error de permisos
if os.IsPermission(errors.Unwrap(err)) {
fmt.Println("Error: Permisos insuficientes para acceder a la ruta")
os.Exit(3)
}
// Otro error de descubrimiento
fmt.Printf("Error de descubrimiento: %v\n", err)
os.Exit(1)
}
fmt.Printf("Archivos escaneados: %d\n", result.Manifest.TotalFiles)
}
Error de Parsing¶
Los errores de parsing no detienen el escaneo. Los archivos que no se pueden parsear se ignoran silenciosamente, pero puedes detectar la situacion:
package main
import (
"context"
"fmt"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
cfg := scanner.DefaultConfig()
cfg.Include = []string{"**/*.py"}
s := scanner.New(cfg)
result, err := s.Scan(context.Background(), "./src")
if err != nil {
// Los errores de parsing no llegan aqui
// Solo errores fatales
panic(err)
}
// Verificar si hay archivos que no se parsearon
// Comparar archivos descubiertos vs archivos en manifest
fmt.Printf("Archivos en manifest: %d\n", result.Manifest.TotalFiles)
// Los archivos que fallaron en parsing no estaran en el manifest
// pero habran sido descubiertos inicialmente
}
Manejo de Errores Parciales¶
Algunos errores permiten continuar con resultados parciales:
package main
import (
"context"
"fmt"
"log"
"strings"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func scanWithPartialResults(path string) (*scanner.Result, []string) {
s := scanner.New(scanner.DefaultConfig())
var warnings []string
result, err := s.Scan(context.Background(), path)
if err != nil {
// Analizar el error
errMsg := err.Error()
if strings.Contains(errMsg, "discovery failed") {
// Error fatal de descubrimiento
log.Fatalf("No se puede escanear: %v", err)
}
if strings.Contains(errMsg, "parsing failed") {
// Algunos archivos fallaron pero otros pueden estar OK
warnings = append(warnings, fmt.Sprintf("Parsing parcial: %v", err))
}
if strings.Contains(errMsg, "surface extraction failed") {
// Fallo extraccion de superficie
warnings = append(warnings, fmt.Sprintf("Superficie MCP incompleta: %v", err))
}
}
return result, warnings
}
func main() {
result, warnings := scanWithPartialResults("./src")
if len(warnings) > 0 {
fmt.Println("Advertencias:")
for _, w := range warnings {
fmt.Printf(" - %s\n", w)
}
}
if result != nil {
fmt.Printf("Hallazgos: %d\n", len(result.Findings))
}
}
Errores de Contexto¶
Timeout¶
package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
cfg := scanner.DefaultConfig()
cfg.Timeout = 30 * time.Second // Timeout corto para ejemplo
s := scanner.New(cfg)
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
result, err := s.Scan(ctx, "./proyecto-grande")
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Printf("Error: El escaneo excedio el timeout de %v\n", cfg.Timeout)
fmt.Println("Sugerencias:")
fmt.Println(" - Aumentar el timeout con --timeout")
fmt.Println(" - Usar modo fast en lugar de deep")
fmt.Println(" - Excluir directorios no relevantes")
os.Exit(124) // Codigo estandar para timeout
}
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Completado en %v\n", result.ScanDuration)
}
Cancelacion¶
package main
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
s := scanner.New(scanner.DefaultConfig())
ctx, cancel := context.WithCancel(context.Background())
// Manejar Ctrl+C
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigChan
fmt.Printf("\nRecibida senal %v, cancelando escaneo...\n", sig)
cancel()
}()
result, err := s.Scan(ctx, "./src")
if err != nil {
if errors.Is(err, context.Canceled) {
fmt.Println("Escaneo cancelado por el usuario")
// No es un error, es una accion del usuario
os.Exit(130) // 128 + SIGINT(2)
}
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Hallazgos: %d\n", len(result.Findings))
}
Timeout Personalizado por Fase¶
package main
import (
"context"
"fmt"
"time"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
// ScanWithPhasedTimeout ejecuta un escaneo con control granular de timeout
func ScanWithPhasedTimeout(path string, discoveryTimeout, analysisTimeout time.Duration) (*scanner.Result, error) {
cfg := scanner.DefaultConfig()
// El timeout total es la suma
cfg.Timeout = discoveryTimeout + analysisTimeout
s := scanner.New(cfg)
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
// Iniciar escaneo
start := time.Now()
result, err := s.Scan(ctx, path)
elapsed := time.Since(start)
if err != nil {
return nil, fmt.Errorf("escaneo fallido despues de %v: %w", elapsed, err)
}
return result, nil
}
func main() {
result, err := ScanWithPhasedTimeout(
"./src",
30*time.Second, // Timeout para descubrimiento
5*time.Minute, // Timeout para analisis
)
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Completado: %d hallazgos en %v\n",
len(result.Findings), result.ScanDuration)
}
Errores de Baseline¶
Error al Cargar Baseline¶
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
baselinePath := ".mcp-scan-baseline.json"
// Intentar cargar baseline
baseline, err := scanner.LoadBaseline(baselinePath)
if err != nil {
// Verificar tipo de error
if os.IsNotExist(err) {
fmt.Printf("Advertencia: Baseline '%s' no existe, se escanea sin filtrar\n", baselinePath)
baseline = nil // Continuar sin baseline
} else if errors.Is(err, json.SyntaxError{}) || isSyntaxError(err) {
fmt.Printf("Error: Baseline '%s' tiene formato JSON invalido\n", baselinePath)
fmt.Println("Sugerencias:")
fmt.Println(" - Verificar que el archivo es JSON valido")
fmt.Println(" - Regenerar el baseline con 'mcp-scan baseline generate'")
os.Exit(1)
} else {
fmt.Printf("Error al cargar baseline: %v\n", err)
os.Exit(1)
}
}
// Continuar con escaneo
cfg := scanner.DefaultConfig()
s := scanner.New(cfg)
result, err := s.Scan(context.Background(), "./src")
if err != nil {
fmt.Printf("Error en escaneo: %v\n", err)
os.Exit(1)
}
// Aplicar baseline si existe
if baseline != nil {
filtered := s.ApplyBaseline(result, baseline)
fmt.Printf("Filtrados %d hallazgos por baseline\n", filtered)
}
fmt.Printf("Hallazgos: %d\n", len(result.Findings))
}
func isSyntaxError(err error) bool {
var syntaxErr *json.SyntaxError
return errors.As(err, &syntaxErr)
}
Error al Guardar Baseline¶
package main
import (
"context"
"fmt"
"os"
"path/filepath"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
s := scanner.New(scanner.DefaultConfig())
result, err := s.Scan(context.Background(), "./src")
if err != nil {
fmt.Printf("Error: %v\n", err)
os.Exit(1)
}
baseline := s.GenerateBaseline(result, "Baseline inicial", "user@example.com")
baselinePath := ".mcp-scan-baseline.json"
// Verificar que podemos escribir
dir := filepath.Dir(baselinePath)
if dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
fmt.Printf("Error: No se puede crear directorio '%s': %v\n", dir, err)
os.Exit(1)
}
}
// Intentar guardar
if err := scanner.SaveBaseline(baseline, baselinePath); err != nil {
if os.IsPermission(err) {
fmt.Printf("Error: Sin permisos para escribir '%s'\n", baselinePath)
os.Exit(1)
}
fmt.Printf("Error al guardar baseline: %v\n", err)
os.Exit(1)
}
fmt.Printf("Baseline guardado en %s (%d entradas)\n",
baselinePath, baseline.Len())
}
Errores de Reporte¶
Error al Generar Reporte¶
package main
import (
"context"
"fmt"
"os"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
s := scanner.New(scanner.DefaultConfig())
result, err := s.Scan(context.Background(), "./src")
if err != nil {
fmt.Printf("Error en escaneo: %v\n", err)
os.Exit(1)
}
// Intentar generar reporte en diferentes formatos
formats := []string{"json", "sarif", "evidence"}
for _, format := range formats {
report, err := s.GenerateReport(result, format)
if err != nil {
fmt.Printf("Error generando reporte %s: %v\n", format, err)
continue // Intentar siguiente formato
}
outputFile := fmt.Sprintf("scan-results.%s", formatExtension(format))
if err := os.WriteFile(outputFile, report, 0644); err != nil {
fmt.Printf("Error escribiendo %s: %v\n", outputFile, err)
continue
}
fmt.Printf("Reporte %s guardado en %s\n", format, outputFile)
}
}
func formatExtension(format string) string {
switch format {
case "sarif":
return "sarif"
default:
return "json"
}
}
Patrones de Manejo¶
Wrapper con Reintentos¶
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
type ScanError struct {
Attempts int
LastErr error
}
func (e *ScanError) Error() string {
return fmt.Sprintf("escaneo fallido despues de %d intentos: %v", e.Attempts, e.LastErr)
}
func scanWithRetry(path string, maxAttempts int, backoff time.Duration) (*scanner.Result, error) {
cfg := scanner.DefaultConfig()
s := scanner.New(cfg)
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
result, err := s.Scan(ctx, path)
cancel()
if err == nil {
return result, nil
}
lastErr = err
// No reintentar en errores no recuperables
if errors.Is(err, context.Canceled) {
return nil, err
}
if isPathError(err) {
return nil, err
}
// Esperar antes de reintentar
if attempt < maxAttempts {
wait := backoff * time.Duration(attempt)
log.Printf("Intento %d fallido, reintentando en %v...", attempt, wait)
time.Sleep(wait)
}
}
return nil, &ScanError{
Attempts: maxAttempts,
LastErr: lastErr,
}
}
func isPathError(err error) bool {
return err != nil && (errors.Is(err, errors.New("no such file")) ||
errors.Is(err, errors.New("permission denied")))
}
func main() {
result, err := scanWithRetry("./src", 3, 5*time.Second)
if err != nil {
var scanErr *ScanError
if errors.As(err, &scanErr) {
fmt.Printf("Fallo persistente: %v\n", scanErr)
} else {
fmt.Printf("Error: %v\n", err)
}
return
}
fmt.Printf("Hallazgos: %d\n", len(result.Findings))
}
Manejo Estructurado de Errores¶
package main
import (
"context"
"errors"
"fmt"
"os"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
// ExitCode representa codigos de salida estandarizados
type ExitCode int
const (
ExitSuccess ExitCode = 0
ExitScanError ExitCode = 1
ExitFindingsFound ExitCode = 2
ExitPathNotFound ExitCode = 3
ExitPermissionError ExitCode = 4
ExitTimeout ExitCode = 124
ExitCanceled ExitCode = 130
)
type ScanOutcome struct {
Result *scanner.Result
Error error
ExitCode ExitCode
Message string
}
func runScan(path string) ScanOutcome {
s := scanner.New(scanner.DefaultConfig())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
result, err := s.Scan(ctx, path)
if err != nil {
return classifyError(err)
}
// Verificar si hay hallazgos
if result.ExitCode != 0 {
return ScanOutcome{
Result: result,
ExitCode: ExitFindingsFound,
Message: fmt.Sprintf("Se encontraron %d hallazgos", len(result.Findings)),
}
}
return ScanOutcome{
Result: result,
ExitCode: ExitSuccess,
Message: "Escaneo completado sin hallazgos bloqueantes",
}
}
func classifyError(err error) ScanOutcome {
switch {
case errors.Is(err, context.DeadlineExceeded):
return ScanOutcome{
Error: err,
ExitCode: ExitTimeout,
Message: "El escaneo excedio el tiempo limite",
}
case errors.Is(err, context.Canceled):
return ScanOutcome{
Error: err,
ExitCode: ExitCanceled,
Message: "Escaneo cancelado",
}
case os.IsNotExist(errors.Unwrap(err)):
return ScanOutcome{
Error: err,
ExitCode: ExitPathNotFound,
Message: "Ruta no encontrada",
}
case os.IsPermission(errors.Unwrap(err)):
return ScanOutcome{
Error: err,
ExitCode: ExitPermissionError,
Message: "Permisos insuficientes",
}
default:
return ScanOutcome{
Error: err,
ExitCode: ExitScanError,
Message: "Error en el escaneo",
}
}
}
func main() {
outcome := runScan("./src")
// Logging
if outcome.Error != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", outcome.Message)
fmt.Fprintf(os.Stderr, "Detalles: %v\n", outcome.Error)
} else {
fmt.Println(outcome.Message)
if outcome.Result != nil {
fmt.Printf("Archivos: %d\n", outcome.Result.Manifest.TotalFiles)
fmt.Printf("Hallazgos: %d\n", len(outcome.Result.Findings))
fmt.Printf("MSSS: %.1f\n", outcome.Result.MSSSScore.Total)
}
}
os.Exit(int(outcome.ExitCode))
}
Logging y Diagnostico¶
Logger Estructurado¶
package main
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"time"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
// Configurar logger estructurado
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
logger.Info("Iniciando escaneo",
slog.String("path", "./src"),
slog.String("mode", "fast"))
cfg := scanner.DefaultConfig()
s := scanner.New(cfg)
start := time.Now()
ctx := context.Background()
result, err := s.Scan(ctx, "./src")
duration := time.Since(start)
if err != nil {
logger.Error("Escaneo fallido",
slog.String("error", err.Error()),
slog.Duration("duration", duration))
os.Exit(1)
}
logger.Info("Escaneo completado",
slog.Duration("duration", duration),
slog.Int("files", result.Manifest.TotalFiles),
slog.Int("findings", len(result.Findings)),
slog.Float64("msss_score", result.MSSSScore.Total),
slog.Int("msss_level", result.MSSSScore.Level))
// Log detallado de hallazgos criticos
for _, f := range result.Findings {
if f.Severity == "critical" {
logger.Warn("Hallazgo critico detectado",
slog.String("rule_id", f.RuleID),
slog.String("file", f.Location.File),
slog.Int("line", f.Location.StartLine),
slog.String("title", f.Title))
}
}
}
Reporte de Diagnostico¶
package main
import (
"context"
"fmt"
"os"
"runtime"
"time"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
type DiagnosticReport struct {
Timestamp time.Time `json:"timestamp"`
System SystemInfo `json:"system"`
Config scanner.Config `json:"config"`
ScanResult *ScanDiagnostic `json:"scan_result,omitempty"`
Error *ErrorDiagnostic `json:"error,omitempty"`
}
type SystemInfo struct {
OS string `json:"os"`
Arch string `json:"arch"`
GoVersion string `json:"go_version"`
NumCPU int `json:"num_cpu"`
}
type ScanDiagnostic struct {
Duration string `json:"duration"`
Files int `json:"files"`
Findings int `json:"findings"`
MSSSScore float64 `json:"msss_score"`
MemoryUsed uint64 `json:"memory_used_bytes"`
}
type ErrorDiagnostic struct {
Type string `json:"type"`
Message string `json:"message"`
}
func runWithDiagnostics(path string) *DiagnosticReport {
report := &DiagnosticReport{
Timestamp: time.Now().UTC(),
System: SystemInfo{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
GoVersion: runtime.Version(),
NumCPU: runtime.NumCPU(),
},
}
cfg := scanner.DefaultConfig()
report.Config = cfg
s := scanner.New(cfg)
ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
defer cancel()
result, err := s.Scan(ctx, path)
if err != nil {
report.Error = &ErrorDiagnostic{
Type: fmt.Sprintf("%T", err),
Message: err.Error(),
}
return report
}
var m runtime.MemStats
runtime.ReadMemStats(&m)
report.ScanResult = &ScanDiagnostic{
Duration: result.ScanDuration.String(),
Files: result.Manifest.TotalFiles,
Findings: len(result.Findings),
MSSSScore: result.MSSSScore.Total,
MemoryUsed: m.Alloc,
}
return report
}
func main() {
path := "./src"
if len(os.Args) > 1 {
path = os.Args[1]
}
report := runWithDiagnostics(path)
// Imprimir diagnostico
fmt.Println("=== Diagnostico de Escaneo ===")
fmt.Printf("Fecha: %v\n", report.Timestamp)
fmt.Printf("Sistema: %s/%s (%d CPUs)\n",
report.System.OS, report.System.Arch, report.System.NumCPU)
if report.Error != nil {
fmt.Printf("\nError: %s\n", report.Error.Message)
fmt.Printf("Tipo: %s\n", report.Error.Type)
}
if report.ScanResult != nil {
fmt.Printf("\nResultados:\n")
fmt.Printf(" Duracion: %s\n", report.ScanResult.Duration)
fmt.Printf(" Archivos: %d\n", report.ScanResult.Files)
fmt.Printf(" Hallazgos: %d\n", report.ScanResult.Findings)
fmt.Printf(" MSSS Score: %.1f\n", report.ScanResult.MSSSScore)
fmt.Printf(" Memoria usada: %.2f MB\n",
float64(report.ScanResult.MemoryUsed)/1024/1024)
}
}