Skip to content

Extension de mcp-scan

Esta guia explica como extender mcp-scan con reglas personalizadas y reporteros custom.


Indice


Reglas Personalizadas via Configuracion

mcp-scan soporta reglas personalizadas definidas en el archivo de configuracion YAML. Esta es la forma recomendada de extender las capacidades de deteccion sin modificar el codigo fuente.

Estructura de una Regla Custom

# .mcp-scan.yaml
rules:
  custom:
    - id: "CUSTOM-001"
      pattern: "patron_regex"
      severity: high
      confidence: medium
      class: A
      description: "Descripcion del hallazgo"
      remediation: "Como corregir el problema"
      languages:
        - python
        - javascript

Campos de la Regla

Campo Tipo Requerido Descripcion
id string Si Identificador unico (recomendado: CUSTOM-XXX)
pattern string Si Expresion regular para detectar
severity string Si info, low, medium, high, critical
confidence string Si low, medium, high
class string Si Clase de vulnerabilidad (A-N)
description string Si Descripcion del problema
remediation string No Instrucciones de correccion
languages []string No Lenguajes donde aplicar (vacio = todos)

Ejemplos de Reglas Personalizadas

Detectar Uso de Funciones Deprecadas

rules:
  custom:
    - id: "CUSTOM-DEP001"
      pattern: "deprecated_function\\("
      severity: low
      confidence: high
      class: L
      description: "Uso de funcion deprecada deprecated_function()"
      remediation: "Reemplazar con new_function()"
      languages:
        - python

    - id: "CUSTOM-DEP002"
      pattern: "old_api\\.method\\("
      severity: medium
      confidence: high
      class: L
      description: "Uso de API obsoleta old_api.method()"
      remediation: "Migrar a new_api.method()"
      languages:
        - python
        - javascript

Detectar Configuraciones Inseguras

rules:
  custom:
    - id: "CUSTOM-CFG001"
      pattern: "debug\\s*=\\s*[Tt]rue"
      severity: medium
      confidence: medium
      class: E
      description: "Modo debug activado en produccion"
      remediation: "Desactivar modo debug en entornos de produccion"

    - id: "CUSTOM-CFG002"
      pattern: "verify\\s*=\\s*[Ff]alse"
      severity: high
      confidence: high
      class: F
      description: "Verificacion SSL deshabilitada"
      remediation: "Habilitar verificacion SSL con verify=True"
      languages:
        - python

Detectar Patrones Especificos de la Organizacion

rules:
  custom:
    - id: "ACME-SEC001"
      pattern: "AcmeInternalAPI\\(\\s*api_key\\s*="
      severity: critical
      confidence: high
      class: E
      description: "API key de Acme hardcodeada en codigo"
      remediation: "Usar variable de entorno ACME_API_KEY"

    - id: "ACME-SEC002"
      pattern: "connect\\([\"']acme-internal\\."
      severity: high
      confidence: medium
      class: C
      description: "Conexion directa a infraestructura interna de Acme"
      remediation: "Usar el gateway de API interno"

Detectar Patrones de Inyeccion en Tools MCP

rules:
  custom:
    - id: "CUSTOM-MCP001"
      pattern: "@tool\\s*\\n.*\\n.*eval\\("
      severity: critical
      confidence: high
      class: A
      description: "Tool MCP con eval() - posible RCE"
      remediation: "Eliminar uso de eval() en handlers de tools MCP"
      languages:
        - python

    - id: "CUSTOM-MCP002"
      pattern: "@tool\\s*\\n.*\\n.*subprocess\\.call\\("
      severity: critical
      confidence: high
      class: A
      description: "Tool MCP ejecuta comandos via subprocess.call()"
      remediation: "Usar subprocess.run() con shell=False y validar entrada"
      languages:
        - python

Usar Reglas Custom en Codigo

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/mcphub/mcp-scan/pkg/scanner"
    "gopkg.in/yaml.v3"
)

// CustomRule representa una regla personalizada
type CustomRule struct {
    ID          string   `yaml:"id"`
    Pattern     string   `yaml:"pattern"`
    Severity    string   `yaml:"severity"`
    Confidence  string   `yaml:"confidence"`
    Class       string   `yaml:"class"`
    Description string   `yaml:"description"`
    Remediation string   `yaml:"remediation"`
    Languages   []string `yaml:"languages"`
}

type Config struct {
    Rules struct {
        Custom []CustomRule `yaml:"custom"`
    } `yaml:"rules"`
}

func loadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }

    var cfg Config
    if err := yaml.Unmarshal(data, &cfg); err != nil {
        return nil, err
    }

    return &cfg, nil
}

func main() {
    // Cargar configuracion con reglas custom
    cfg, err := loadConfig(".mcp-scan.yaml")
    if err != nil {
        log.Printf("No se pudo cargar config: %v", err)
    } else {
        fmt.Printf("Reglas custom cargadas: %d\n", len(cfg.Rules.Custom))
        for _, rule := range cfg.Rules.Custom {
            fmt.Printf("  - %s: %s\n", rule.ID, rule.Description)
        }
    }

    // El scanner cargara automaticamente las reglas del archivo de config
    s := scanner.New(scanner.DefaultConfig())

    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    // Los hallazgos incluiran matches de reglas custom
    for _, f := range result.Findings {
        if f.RuleID[:6] == "CUSTOM" || f.RuleID[:4] == "ACME" {
            fmt.Printf("[CUSTOM] %s: %s\n", f.RuleID, f.Description)
        }
    }
}

Reporteros Personalizados

mcp-scan genera reportes en JSON, SARIF y Evidence Bundle. Puedes crear reporteros personalizados procesando el resultado del escaneo.

Estructura Base de un Reportero

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

// Reporter es la interfaz para reporteros personalizados
type Reporter interface {
    Generate(result *scanner.Result) ([]byte, error)
    Write(result *scanner.Result, w io.Writer) error
}

// BaseReporter proporciona funcionalidad comun
type BaseReporter struct {
    RedactSnippets bool
}

func (r *BaseReporter) redact(s string) string {
    if r.RedactSnippets {
        return "[REDACTED]"
    }
    return s
}

Reportero CSV

package main

import (
    "bytes"
    "context"
    "encoding/csv"
    "fmt"
    "io"
    "log"
    "os"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

type CSVReporter struct {
    BaseReporter
    IncludeHeaders bool
}

func NewCSVReporter(redact, headers bool) *CSVReporter {
    return &CSVReporter{
        BaseReporter:   BaseReporter{RedactSnippets: redact},
        IncludeHeaders: headers,
    }
}

func (r *CSVReporter) Generate(result *scanner.Result) ([]byte, error) {
    var buf bytes.Buffer
    if err := r.Write(result, &buf); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

func (r *CSVReporter) Write(result *scanner.Result, w io.Writer) error {
    writer := csv.NewWriter(w)
    defer writer.Flush()

    if r.IncludeHeaders {
        headers := []string{
            "ID", "Rule", "Severity", "Confidence", "Class",
            "File", "Line", "Title", "Description",
        }
        if err := writer.Write(headers); err != nil {
            return err
        }
    }

    for _, f := range result.Findings {
        row := []string{
            f.ID,
            f.RuleID,
            string(f.Severity),
            string(f.Confidence),
            string(f.Class),
            f.Location.File,
            fmt.Sprintf("%d", f.Location.StartLine),
            f.Title,
            f.Description,
        }
        if err := writer.Write(row); err != nil {
            return err
        }
    }

    return nil
}

func main() {
    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    reporter := NewCSVReporter(false, true)

    // Escribir a archivo
    f, _ := os.Create("report.csv")
    defer f.Close()
    reporter.Write(result, f)

    fmt.Println("Reporte CSV generado en report.csv")
}

Reportero Markdown

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "log"
    "os"
    "strings"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

type MarkdownReporter struct {
    BaseReporter
    IncludeSummary   bool
    IncludeMCPSurface bool
}

func NewMarkdownReporter() *MarkdownReporter {
    return &MarkdownReporter{
        IncludeSummary:    true,
        IncludeMCPSurface: true,
    }
}

func (r *MarkdownReporter) Generate(result *scanner.Result) ([]byte, error) {
    var buf bytes.Buffer
    if err := r.Write(result, &buf); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

func (r *MarkdownReporter) Write(result *scanner.Result, w io.Writer) error {
    // Titulo
    fmt.Fprintln(w, "# Reporte de Seguridad MCP")
    fmt.Fprintln(w)

    // Resumen
    if r.IncludeSummary {
        fmt.Fprintln(w, "## Resumen")
        fmt.Fprintln(w)
        fmt.Fprintf(w, "| Metrica | Valor |\n")
        fmt.Fprintf(w, "|---------|-------|\n")
        fmt.Fprintf(w, "| Archivos escaneados | %d |\n", result.Manifest.TotalFiles)
        fmt.Fprintf(w, "| Total hallazgos | %d |\n", len(result.Findings))
        fmt.Fprintf(w, "| MSSS Score | %.1f/100 |\n", result.MSSSScore.Total)
        fmt.Fprintf(w, "| Nivel MSSS | %d |\n", result.MSSSScore.Level)
        fmt.Fprintf(w, "| Duracion | %v |\n", result.ScanDuration)
        fmt.Fprintln(w)

        // Por severidad
        fmt.Fprintln(w, "### Por Severidad")
        fmt.Fprintln(w)
        fmt.Fprintln(w, "| Severidad | Cantidad |")
        fmt.Fprintln(w, "|-----------|----------|")
        for sev, count := range result.Summary.BySeverity {
            emoji := severityEmoji(sev)
            fmt.Fprintf(w, "| %s %s | %d |\n", emoji, sev, count)
        }
        fmt.Fprintln(w)
    }

    // Superficie MCP
    if r.IncludeMCPSurface && result.MCPSurface != nil {
        fmt.Fprintln(w, "## Superficie MCP")
        fmt.Fprintln(w)
        fmt.Fprintf(w, "- **Transporte:** %s\n", result.MCPSurface.Transport)
        fmt.Fprintf(w, "- **Tools:** %d\n", len(result.MCPSurface.Tools))
        fmt.Fprintf(w, "- **Resources:** %d\n", len(result.MCPSurface.Resources))
        fmt.Fprintf(w, "- **Prompts:** %d\n", len(result.MCPSurface.Prompts))
        fmt.Fprintln(w)

        if len(result.MCPSurface.Tools) > 0 {
            fmt.Fprintln(w, "### Tools Detectados")
            fmt.Fprintln(w)
            for _, tool := range result.MCPSurface.Tools {
                fmt.Fprintf(w, "- **%s**", tool.Name)
                if tool.Description != "" {
                    fmt.Fprintf(w, ": %s", tool.Description)
                }
                fmt.Fprintln(w)
            }
            fmt.Fprintln(w)
        }
    }

    // Hallazgos
    fmt.Fprintln(w, "## Hallazgos")
    fmt.Fprintln(w)

    if len(result.Findings) == 0 {
        fmt.Fprintln(w, "No se encontraron hallazgos de seguridad.")
        return nil
    }

    // Agrupar por severidad
    bySeverity := groupBySeverity(result.Findings)

    severityOrder := []string{"critical", "high", "medium", "low", "info"}
    for _, sev := range severityOrder {
        findings := bySeverity[sev]
        if len(findings) == 0 {
            continue
        }

        fmt.Fprintf(w, "### %s %s (%d)\n\n", severityEmoji(sev), strings.Title(sev), len(findings))

        for _, f := range findings {
            fmt.Fprintf(w, "#### %s - %s\n\n", f.RuleID, f.Title)
            fmt.Fprintf(w, "**Ubicacion:** `%s:%d`\n\n", f.Location.File, f.Location.StartLine)
            fmt.Fprintf(w, "%s\n\n", f.Description)

            if f.Evidence.Snippet != "" && !r.RedactSnippets {
                fmt.Fprintln(w, "```")
                fmt.Fprintln(w, f.Evidence.Snippet)
                fmt.Fprintln(w, "```")
                fmt.Fprintln(w)
            }

            if f.Remediation != "" {
                fmt.Fprintf(w, "**Remediacion:** %s\n\n", f.Remediation)
            }

            fmt.Fprintln(w, "---")
            fmt.Fprintln(w)
        }
    }

    return nil
}

func severityEmoji(sev string) string {
    switch sev {
    case "critical":
        return "[CRIT]"
    case "high":
        return "[HIGH]"
    case "medium":
        return "[MED]"
    case "low":
        return "[LOW]"
    default:
        return "[INFO]"
    }
}

func groupBySeverity(findings []types.Finding) map[string][]types.Finding {
    groups := make(map[string][]types.Finding)
    for _, f := range findings {
        groups[string(f.Severity)] = append(groups[string(f.Severity)], f)
    }
    return groups
}

func main() {
    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    reporter := NewMarkdownReporter()

    f, _ := os.Create("SECURITY_REPORT.md")
    defer f.Close()
    reporter.Write(result, f)

    fmt.Println("Reporte Markdown generado en SECURITY_REPORT.md")
}

Reportero XML (JUnit)

Util para integracion con sistemas CI/CD que esperan formato JUnit:

package main

import (
    "context"
    "encoding/xml"
    "fmt"
    "log"
    "os"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

type JUnitTestSuites struct {
    XMLName    xml.Name         `xml:"testsuites"`
    TestSuites []JUnitTestSuite `xml:"testsuite"`
}

type JUnitTestSuite struct {
    Name     string          `xml:"name,attr"`
    Tests    int             `xml:"tests,attr"`
    Failures int             `xml:"failures,attr"`
    Errors   int             `xml:"errors,attr"`
    Time     float64         `xml:"time,attr"`
    Cases    []JUnitTestCase `xml:"testcase"`
}

type JUnitTestCase struct {
    Name      string        `xml:"name,attr"`
    ClassName string        `xml:"classname,attr"`
    Time      float64       `xml:"time,attr"`
    Failure   *JUnitFailure `xml:"failure,omitempty"`
}

type JUnitFailure struct {
    Message string `xml:"message,attr"`
    Type    string `xml:"type,attr"`
    Content string `xml:",chardata"`
}

type JUnitReporter struct{}

func (r *JUnitReporter) Generate(result *scanner.Result) ([]byte, error) {
    suites := JUnitTestSuites{
        TestSuites: []JUnitTestSuite{
            {
                Name:     "MCP Security Scan",
                Tests:    len(result.Findings) + 1,  // +1 para test de compliance
                Failures: len(result.Findings),
                Time:     result.ScanDuration.Seconds(),
            },
        },
    }

    suite := &suites.TestSuites[0]

    // Agregar caso de compliance
    complianceCase := JUnitTestCase{
        Name:      "MSSS Compliance",
        ClassName: "security.msss",
        Time:      0,
    }
    if result.MSSSScore.Level < 1 {
        complianceCase.Failure = &JUnitFailure{
            Message: fmt.Sprintf("MSSS Level %d < 1", result.MSSSScore.Level),
            Type:    "ComplianceFailure",
            Content: fmt.Sprintf("Score: %.1f, Level: %d",
                result.MSSSScore.Total, result.MSSSScore.Level),
        }
        suite.Failures++
    }
    suite.Cases = append(suite.Cases, complianceCase)

    // Agregar hallazgos como test cases
    for _, f := range result.Findings {
        tc := JUnitTestCase{
            Name:      f.RuleID,
            ClassName: fmt.Sprintf("security.%s", f.Class),
            Time:      0,
            Failure: &JUnitFailure{
                Message: f.Title,
                Type:    string(f.Severity),
                Content: fmt.Sprintf("%s\nFile: %s:%d\n%s",
                    f.Description,
                    f.Location.File, f.Location.StartLine,
                    f.Remediation),
            },
        }
        suite.Cases = append(suite.Cases, tc)
    }

    return xml.MarshalIndent(suites, "", "  ")
}

func main() {
    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    reporter := &JUnitReporter{}
    output, err := reporter.Generate(result)
    if err != nil {
        log.Fatal(err)
    }

    os.WriteFile("junit-results.xml", output, 0644)
    fmt.Println("Reporte JUnit generado en junit-results.xml")
}

Procesadores de Resultados

Puedes crear procesadores que transforman o enriquecen los resultados del escaneo.

Procesador de Enriquecimiento

package main

import (
    "context"
    "fmt"
    "log"
    "strings"

    "github.com/mcphub/mcp-scan/pkg/scanner"
    "github.com/mcphub/mcp-scan/internal/types"
)

// FindingEnricher enriquece hallazgos con informacion adicional
type FindingEnricher struct {
    OwnershipMap map[string]string  // file pattern -> owner
    PriorityMap  map[string]int     // rule -> priority
}

func NewFindingEnricher() *FindingEnricher {
    return &FindingEnricher{
        OwnershipMap: map[string]string{
            "auth/":     "security-team@example.com",
            "payments/": "payments-team@example.com",
            "api/":      "backend-team@example.com",
        },
        PriorityMap: map[string]int{
            "MCP-A003": 1,  // RCE - maxima prioridad
            "MCP-E001": 2,  // Secrets
            "MCP-C002": 3,  // SSRF
        },
    }
}

type EnrichedFinding struct {
    types.Finding
    Owner    string `json:"owner"`
    Priority int    `json:"priority"`
    Tags     []string `json:"tags"`
}

func (e *FindingEnricher) Enrich(findings []types.Finding) []EnrichedFinding {
    enriched := make([]EnrichedFinding, len(findings))

    for i, f := range findings {
        ef := EnrichedFinding{
            Finding:  f,
            Priority: 99, // Default
        }

        // Asignar owner
        for pattern, owner := range e.OwnershipMap {
            if strings.Contains(f.Location.File, pattern) {
                ef.Owner = owner
                break
            }
        }

        // Asignar prioridad
        if priority, ok := e.PriorityMap[f.RuleID]; ok {
            ef.Priority = priority
        }

        // Agregar tags
        ef.Tags = e.generateTags(f)

        enriched[i] = ef
    }

    return enriched
}

func (e *FindingEnricher) generateTags(f types.Finding) []string {
    var tags []string

    // Tag por severidad
    tags = append(tags, "severity:"+string(f.Severity))

    // Tag por clase
    tags = append(tags, "class:"+string(f.Class))

    // Tag si esta en contexto MCP
    if f.MCPContext != nil {
        tags = append(tags, "mcp-tool")
        if f.MCPContext.ToolName != "" {
            tags = append(tags, "tool:"+f.MCPContext.ToolName)
        }
    }

    // Tag si tiene traza de taint
    if f.Trace != nil {
        tags = append(tags, "taint-flow")
    }

    return tags
}

func main() {
    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    enricher := NewFindingEnricher()
    enrichedFindings := enricher.Enrich(result.Findings)

    for _, ef := range enrichedFindings {
        fmt.Printf("[P%d] %s - %s\n", ef.Priority, ef.RuleID, ef.Title)
        fmt.Printf("  Owner: %s\n", ef.Owner)
        fmt.Printf("  Tags: %v\n", ef.Tags)
    }
}

Procesador de Filtrado Avanzado

package main

import (
    "context"
    "fmt"
    "log"
    "regexp"

    "github.com/mcphub/mcp-scan/pkg/scanner"
    "github.com/mcphub/mcp-scan/internal/types"
)

// FindingFilter permite filtrado avanzado de hallazgos
type FindingFilter struct {
    MinSeverity     types.Severity
    MinConfidence   types.Confidence
    IncludeClasses  []types.VulnClass
    ExcludeClasses  []types.VulnClass
    IncludeRules    []string
    ExcludeRules    []string
    FilePatterns    []*regexp.Regexp
    ExcludePatterns []*regexp.Regexp
    OnlyMCPContext  bool
    OnlyTaintFlow   bool
}

func (f *FindingFilter) Filter(findings []types.Finding) []types.Finding {
    var filtered []types.Finding

    for _, finding := range findings {
        if f.shouldInclude(finding) {
            filtered = append(filtered, finding)
        }
    }

    return filtered
}

func (f *FindingFilter) shouldInclude(finding types.Finding) bool {
    // Filtro por severidad minima
    if f.MinSeverity != "" && finding.Severity.Level() < f.MinSeverity.Level() {
        return false
    }

    // Filtro por confianza minima
    if f.MinConfidence != "" {
        confLevel := map[types.Confidence]int{
            types.ConfidenceLow:    1,
            types.ConfidenceMedium: 2,
            types.ConfidenceHigh:   3,
        }
        if confLevel[finding.Confidence] < confLevel[f.MinConfidence] {
            return false
        }
    }

    // Filtro por clases incluidas
    if len(f.IncludeClasses) > 0 {
        included := false
        for _, class := range f.IncludeClasses {
            if finding.Class == class {
                included = true
                break
            }
        }
        if !included {
            return false
        }
    }

    // Filtro por clases excluidas
    for _, class := range f.ExcludeClasses {
        if finding.Class == class {
            return false
        }
    }

    // Filtro por reglas excluidas
    for _, rule := range f.ExcludeRules {
        if finding.RuleID == rule {
            return false
        }
    }

    // Filtro por patrones de archivo
    if len(f.FilePatterns) > 0 {
        matched := false
        for _, pattern := range f.FilePatterns {
            if pattern.MatchString(finding.Location.File) {
                matched = true
                break
            }
        }
        if !matched {
            return false
        }
    }

    // Filtro por patrones excluidos
    for _, pattern := range f.ExcludePatterns {
        if pattern.MatchString(finding.Location.File) {
            return false
        }
    }

    // Filtro solo contexto MCP
    if f.OnlyMCPContext && finding.MCPContext == nil {
        return false
    }

    // Filtro solo con flujo de taint
    if f.OnlyTaintFlow && finding.Trace == nil {
        return false
    }

    return true
}

func main() {
    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    // Crear filtro: solo criticos/altos en tools MCP
    filter := &FindingFilter{
        MinSeverity:    types.SeverityHigh,
        OnlyMCPContext: true,
        ExcludeRules:   []string{"MCP-E002"},  // Excluir regla especifica
    }

    filtered := filter.Filter(result.Findings)

    fmt.Printf("Original: %d, Filtrado: %d\n",
        len(result.Findings), len(filtered))

    for _, f := range filtered {
        fmt.Printf("[%s] %s en tool %s\n",
            f.Severity, f.RuleID, f.MCPContext.ToolName)
    }
}

Integracion con Sistemas Externos

Exportar a Jira

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"

    "github.com/mcphub/mcp-scan/pkg/scanner"
    "github.com/mcphub/mcp-scan/internal/types"
)

type JiraClient struct {
    BaseURL  string
    Username string
    Token    string
    Project  string
}

type JiraIssue struct {
    Fields JiraFields `json:"fields"`
}

type JiraFields struct {
    Project   JiraProject `json:"project"`
    Summary   string      `json:"summary"`
    Description string    `json:"description"`
    IssueType JiraType    `json:"issuetype"`
    Priority  JiraPriority `json:"priority,omitempty"`
    Labels    []string    `json:"labels,omitempty"`
}

type JiraProject struct {
    Key string `json:"key"`
}

type JiraType struct {
    Name string `json:"name"`
}

type JiraPriority struct {
    Name string `json:"name"`
}

func (c *JiraClient) CreateIssue(finding types.Finding) error {
    issue := JiraIssue{
        Fields: JiraFields{
            Project:   JiraProject{Key: c.Project},
            Summary:   fmt.Sprintf("[%s] %s - %s", finding.Severity, finding.RuleID, finding.Title),
            Description: c.formatDescription(finding),
            IssueType: JiraType{Name: "Bug"},
            Priority:  c.mapPriority(finding.Severity),
            Labels:    []string{"security", "mcp-scan", string(finding.Class)},
        },
    }

    payload, _ := json.Marshal(issue)

    req, err := http.NewRequest("POST", c.BaseURL+"/rest/api/2/issue", bytes.NewReader(payload))
    if err != nil {
        return err
    }

    req.SetBasicAuth(c.Username, c.Token)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return fmt.Errorf("jira API error: %s", resp.Status)
    }

    return nil
}

func (c *JiraClient) formatDescription(f types.Finding) string {
    desc := fmt.Sprintf(`
h2. Hallazgo de Seguridad

*Regla:* %s
*Severidad:* %s
*Confianza:* %s
*Clase:* %s

h3. Descripcion
%s

h3. Ubicacion
* Archivo: %s
* Linea: %d

`, f.RuleID, f.Severity, f.Confidence, f.Class, f.Description, f.Location.File, f.Location.StartLine)

    if f.Remediation != "" {
        desc += fmt.Sprintf(`
h3. Remediacion
%s
`, f.Remediation)
    }

    if f.Evidence.Snippet != "" {
        desc += fmt.Sprintf(`
h3. Codigo
{code}
%s
{code}
`, f.Evidence.Snippet)
    }

    return desc
}

func (c *JiraClient) mapPriority(sev types.Severity) JiraPriority {
    priorities := map[types.Severity]string{
        types.SeverityCritical: "Highest",
        types.SeverityHigh:     "High",
        types.SeverityMedium:   "Medium",
        types.SeverityLow:      "Low",
        types.SeverityInfo:     "Lowest",
    }
    return JiraPriority{Name: priorities[sev]}
}

func main() {
    jira := &JiraClient{
        BaseURL:  os.Getenv("JIRA_URL"),
        Username: os.Getenv("JIRA_USERNAME"),
        Token:    os.Getenv("JIRA_TOKEN"),
        Project:  os.Getenv("JIRA_PROJECT"),
    }

    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    // Crear issues solo para criticos y altos
    for _, f := range result.Findings {
        if f.Severity == types.SeverityCritical || f.Severity == types.SeverityHigh {
            if err := jira.CreateIssue(f); err != nil {
                log.Printf("Error creando issue para %s: %v", f.RuleID, err)
            } else {
                fmt.Printf("Issue creado para %s\n", f.RuleID)
            }
        }
    }
}

Webhook Generico

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

type WebhookPayload struct {
    EventType  string             `json:"event_type"`
    Timestamp  time.Time          `json:"timestamp"`
    Project    string             `json:"project"`
    Branch     string             `json:"branch"`
    Commit     string             `json:"commit"`
    Summary    WebhookSummary     `json:"summary"`
    Compliance WebhookCompliance  `json:"compliance"`
    Findings   []WebhookFinding   `json:"findings,omitempty"`
}

type WebhookSummary struct {
    TotalFiles    int            `json:"total_files"`
    TotalFindings int            `json:"total_findings"`
    BySeverity    map[string]int `json:"by_severity"`
    Duration      string         `json:"duration"`
}

type WebhookCompliance struct {
    Score     float64 `json:"score"`
    Level     int     `json:"level"`
    Compliant bool    `json:"compliant"`
}

type WebhookFinding struct {
    ID       string `json:"id"`
    Rule     string `json:"rule"`
    Severity string `json:"severity"`
    Title    string `json:"title"`
    File     string `json:"file"`
    Line     int    `json:"line"`
}

func sendWebhook(url string, result *scanner.Result) error {
    payload := WebhookPayload{
        EventType:  "scan_completed",
        Timestamp:  time.Now().UTC(),
        Project:    os.Getenv("PROJECT_NAME"),
        Branch:     os.Getenv("GIT_BRANCH"),
        Commit:     os.Getenv("GIT_COMMIT"),
        Summary: WebhookSummary{
            TotalFiles:    result.Manifest.TotalFiles,
            TotalFindings: len(result.Findings),
            BySeverity:    result.Summary.BySeverity,
            Duration:      result.ScanDuration.String(),
        },
        Compliance: WebhookCompliance{
            Score:     result.MSSSScore.Total,
            Level:     result.MSSSScore.Level,
            Compliant: result.MSSSScore.Compliant,
        },
    }

    // Incluir top 10 hallazgos mas severos
    for i, f := range result.Findings {
        if i >= 10 {
            break
        }
        payload.Findings = append(payload.Findings, WebhookFinding{
            ID:       f.ID,
            Rule:     f.RuleID,
            Severity: string(f.Severity),
            Title:    f.Title,
            File:     f.Location.File,
            Line:     f.Location.StartLine,
        })
    }

    data, _ := json.Marshal(payload)

    resp, err := http.Post(url, "application/json", bytes.NewReader(data))
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        return fmt.Errorf("webhook error: %s", resp.Status)
    }

    return nil
}

func main() {
    webhookURL := os.Getenv("WEBHOOK_URL")
    if webhookURL == "" {
        log.Fatal("WEBHOOK_URL no configurado")
    }

    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    if err := sendWebhook(webhookURL, result); err != nil {
        log.Fatalf("Error enviando webhook: %v", err)
    }

    fmt.Println("Webhook enviado exitosamente")
}