Extension de mcp-scan¶
Esta guia explica como extender mcp-scan con reglas personalizadas y reporteros custom.
Indice¶
- Reglas Personalizadas via Configuracion
- Reporteros Personalizados
- Procesadores de Resultados
- Integracion con Sistemas Externos
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")
}