Files
gohoarder/pkg/app/handlers_vulnerabilities.go
T
lukaszraczylo f72b481720 chore(deps): migrate fiber from v2 to v3
The automated update changed only go.mod, so the build failed on the old v2 imports. This commit moves the code to the v3 API.

The commit replaces BodyParser with Bind().Body, Static with the static middleware, and ListenTLS with ListenConfig. It also updates the tests for TestConfig.

Fixes #179
2026-10-04 00:50:37 +01:00

165 lines
5.1 KiB
Go

package app
import (
"strings"
"github.com/gofiber/fiber/v3"
"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.
// Normalize Go cache-key names (modulePath/@v/version.zip) to the
// natural module path so admin bypass targets like
// "go/github.com/foo/[email protected]" match the package.
bypassName := name
if registry == "go" {
if idx := strings.Index(bypassName, "/@v/"); idx >= 0 {
bypassName = bypassName[:idx]
}
}
packageKey := registry + "/" + bypassName + "@" + version
packageKeyNoVersion := registry + "/" + bypassName
bypassedCVEs := make(map[string]*metadata.CVEBypass)
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[metadata.NormalizeSeverity(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)
}