Files
gohoarder/pkg/app/handlers_vulnerabilities.go
T
2026-01-02 23:14:23 +00:00

157 lines
4.7 KiB
Go

package app
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
"github.com/rs/zerolog/log"
)
// handleVulnerabilities handles /api/packages/{registry}/{name}/{version}/vulnerabilities endpoint
func (a *App) handleVulnerabilities(c *fiber.Ctx) error {
c.Set("Content-Type", "application/json")
c.Set("Access-Control-Allow-Origin", "*")
c.Set("Access-Control-Allow-Methods", "GET, OPTIONS")
c.Set("Access-Control-Allow-Headers", "Content-Type")
if c.Method() == "OPTIONS" {
return c.SendStatus(fiber.StatusOK)
}
if c.Method() != "GET" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
}
ctx := c.Context()
// Parse path: /api/packages/{registry}/{name}/{version}/vulnerabilities
path := strings.TrimPrefix(c.Path(), "/api/packages/")
path = strings.TrimSuffix(path, "/vulnerabilities")
parts := strings.Split(path, "/")
if len(parts) < 3 {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid path format, expected /api/packages/{registry}/{name}/{version}/vulnerabilities",
})
}
registry := parts[0]
version := parts[len(parts)-1]
name := strings.Join(parts[1:len(parts)-1], "/")
log.Debug().
Str("registry", registry).
Str("name", name).
Str("version", version).
Msg("Getting vulnerabilities for package")
// Get scan result from metadata store
scanResult, err := a.metadata.GetScanResult(ctx, registry, name, version)
if err != nil {
// Check if package exists
pkg, pkgErr := a.metadata.GetPackage(ctx, registry, name, version)
if pkgErr != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "package not found"})
}
// Package exists but not scanned yet
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"package": fiber.Map{
"registry": registry,
"name": name,
"version": version,
},
"scanned": false,
"status": "pending",
"vulnerabilities": []interface{}{},
"vulnerability_count": 0,
"message": "Package not yet scanned for vulnerabilities",
"security_scanned": pkg.SecurityScanned,
})
}
// Get active bypasses to show which vulnerabilities are bypassed
bypasses, err := a.metadata.GetActiveCVEBypasses(ctx)
if err != nil {
log.Warn().Err(err).Msg("Failed to get CVE bypasses")
bypasses = []*metadata.CVEBypass{}
}
// Build bypass map for fast lookup
bypassedCVEs := make(map[string]*metadata.CVEBypass)
packageKey := registry + "/" + name + "@" + version
packageKeyNoVersion := registry + "/" + name
for _, bypass := range bypasses {
if bypass.Type == metadata.BypassTypeCVE && bypass.Active {
// Check if bypass applies to this package
if bypass.AppliesTo == "" || bypass.AppliesTo == packageKey || bypass.AppliesTo == packageKeyNoVersion {
bypassedCVEs[strings.ToUpper(bypass.Target)] = bypass
}
}
}
// Enrich vulnerabilities with bypass information
enrichedVulns := make([]map[string]interface{}, 0, len(scanResult.Vulnerabilities))
severityCounts := make(map[string]int)
for _, vuln := range scanResult.Vulnerabilities {
bypassed := false
var bypassInfo map[string]interface{}
// Check if this CVE is bypassed
if bypass, ok := bypassedCVEs[strings.ToUpper(vuln.ID)]; ok {
bypassed = true
bypassInfo = map[string]interface{}{
"id": bypass.ID,
"reason": bypass.Reason,
"created_by": bypass.CreatedBy,
"expires_at": bypass.ExpiresAt,
}
} else {
// Count non-bypassed vulnerabilities by severity
severityCounts[strings.ToUpper(vuln.Severity)]++
}
enrichedVuln := map[string]interface{}{
"id": vuln.ID,
"severity": vuln.Severity,
"title": vuln.Title,
"description": vuln.Description,
"references": vuln.References,
"fixed_in": vuln.FixedIn,
"bypassed": bypassed,
}
if bypassed {
enrichedVuln["bypass"] = bypassInfo
}
enrichedVulns = append(enrichedVulns, enrichedVuln)
}
// Build response
response := fiber.Map{
"package": fiber.Map{
"registry": registry,
"name": name,
"version": version,
},
"scanned": true,
"scanner": scanResult.Scanner,
"scanned_at": scanResult.ScannedAt,
"status": scanResult.Status,
"vulnerabilities": enrichedVulns,
"vulnerability_count": scanResult.VulnerabilityCount,
"severity_counts": fiber.Map{
"critical": severityCounts["CRITICAL"],
"high": severityCounts["HIGH"],
"moderate": severityCounts["MODERATE"],
"low": severityCounts["LOW"],
},
"bypassed_count": len(scanResult.Vulnerabilities) - (severityCounts["CRITICAL"] + severityCounts["HIGH"] + severityCounts["MODERATE"] + severityCounts["LOW"]),
}
return c.Status(fiber.StatusOK).JSON(response)
}