Skip to content

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

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)
    }
}