mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-10 23:29:22 +00:00
fixes
This commit is contained in:
+70
-48
@@ -9,6 +9,8 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/adaptor"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/analytics"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/auth"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/cache"
|
||||
@@ -16,7 +18,6 @@ import (
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/config"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/health"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/lock"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/logger"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
|
||||
metafile "github.com/lukaszraczylo/gohoarder/pkg/metadata/file"
|
||||
metasqlite "github.com/lukaszraczylo/gohoarder/pkg/metadata/sqlite"
|
||||
@@ -36,7 +37,7 @@ import (
|
||||
// App represents the main application
|
||||
type App struct {
|
||||
config *config.Config
|
||||
server *http.Server
|
||||
app *fiber.App
|
||||
healthChecker *health.Checker
|
||||
cache *cache.Manager
|
||||
storage storage.StorageBackend
|
||||
@@ -163,7 +164,7 @@ func (a *App) initializeComponents() error {
|
||||
a.wsServer = websocket.NewServer(websocket.Config{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
CheckOrigin: func(_ *http.Request) bool {
|
||||
return true // Allow all origins in development
|
||||
},
|
||||
})
|
||||
@@ -221,55 +222,60 @@ func (a *App) initializeComponents() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// setupServer sets up the HTTP server and routes
|
||||
// setupServer sets up the Fiber server and routes
|
||||
func (a *App) setupServer() error {
|
||||
mux := http.NewServeMux()
|
||||
// Create Fiber app
|
||||
a.app = fiber.New(fiber.Config{
|
||||
ReadTimeout: a.config.Server.ReadTimeout,
|
||||
WriteTimeout: a.config.Server.WriteTimeout,
|
||||
ServerHeader: "GoHoarder",
|
||||
AppName: "GoHoarder v1.0",
|
||||
})
|
||||
|
||||
// Health and metrics endpoints
|
||||
mux.HandleFunc("/health", a.healthChecker.HealthHandler())
|
||||
mux.HandleFunc("/health/ready", a.healthChecker.ReadyHandler())
|
||||
mux.Handle("/metrics", metrics.Handler())
|
||||
// Health and metrics endpoints (adapted from net/http)
|
||||
a.app.Get("/health", adaptor.HTTPHandlerFunc(a.healthChecker.HealthHandler()))
|
||||
a.app.Get("/health/ready", adaptor.HTTPHandlerFunc(a.healthChecker.ReadyHandler()))
|
||||
a.app.Get("/metrics", adaptor.HTTPHandler(metrics.Handler()))
|
||||
|
||||
// WebSocket endpoint
|
||||
mux.HandleFunc("/ws", a.wsServer.HandleWebSocket)
|
||||
// WebSocket endpoint (adapted from net/http)
|
||||
a.app.Get("/ws", adaptor.HTTPHandlerFunc(a.wsServer.HandleWebSocket))
|
||||
|
||||
// API endpoints
|
||||
mux.HandleFunc("/api/packages/", a.handlePackages) // Handles packages and vulnerabilities
|
||||
mux.HandleFunc("/api/stats", a.handleStats)
|
||||
mux.HandleFunc("/api/info", a.handleInfo)
|
||||
a.app.Get("/api/config", a.handleConfig)
|
||||
a.app.All("/api/packages/*", a.handlePackages) // Handles packages and vulnerabilities
|
||||
a.app.Get("/api/stats", a.handleStats)
|
||||
a.app.Get("/api/stats/timeseries", a.handleTimeSeriesStats)
|
||||
a.app.Get("/api/info", a.handleInfo)
|
||||
|
||||
// Admin endpoints (bypass management)
|
||||
mux.HandleFunc("/api/admin/bypasses/", a.handleBypassByID) // Must come before /api/admin/bypasses
|
||||
mux.HandleFunc("/api/admin/bypasses", a.handleAdminBypasses)
|
||||
a.app.All("/api/admin/bypasses/:id?", a.handleAdminBypasses)
|
||||
|
||||
// Proxy handlers
|
||||
// Proxy handlers (adapted from net/http)
|
||||
goProxyHandler := goproxy.New(a.cache, a.networkClient, goproxy.Config{
|
||||
Upstream: "https://proxy.golang.org",
|
||||
SumDBURL: "https://sum.golang.org",
|
||||
})
|
||||
mux.Handle("/go/", http.StripPrefix("/go", goProxyHandler))
|
||||
a.app.All("/go/*", adaptor.HTTPHandler(http.StripPrefix("/go", goProxyHandler)))
|
||||
|
||||
npmProxyHandler := npm.New(a.cache, a.networkClient, npm.Config{
|
||||
Upstream: "https://registry.npmjs.org",
|
||||
})
|
||||
mux.Handle("/npm/", http.StripPrefix("/npm", npmProxyHandler))
|
||||
a.app.All("/npm/*", adaptor.HTTPHandler(http.StripPrefix("/npm", npmProxyHandler)))
|
||||
|
||||
pypiProxyHandler := pypi.New(a.cache, a.networkClient, pypi.Config{
|
||||
Upstream: "https://pypi.org/simple",
|
||||
})
|
||||
mux.Handle("/pypi/", http.StripPrefix("/pypi", pypiProxyHandler))
|
||||
a.app.All("/pypi/*", adaptor.HTTPHandler(http.StripPrefix("/pypi", pypiProxyHandler)))
|
||||
|
||||
// Serve frontend static files
|
||||
frontendDir := "frontend/dist"
|
||||
if _, err := os.Stat(frontendDir); err == nil {
|
||||
log.Info().Str("dir", frontendDir).Msg("Serving frontend static files")
|
||||
fs := http.FileServer(http.Dir(frontendDir))
|
||||
mux.Handle("/", fs)
|
||||
a.app.Static("/", frontendDir)
|
||||
} else {
|
||||
log.Warn().Msg("Frontend dist directory not found, frontend won't be served")
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
fmt.Fprintf(w, `
|
||||
a.app.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.Type("html").SendString(`
|
||||
<html>
|
||||
<head><title>GoHoarder</title></head>
|
||||
<body>
|
||||
@@ -287,20 +293,9 @@ func (a *App) setupServer() error {
|
||||
})
|
||||
}
|
||||
|
||||
// Wrap with logging middleware
|
||||
handler := logger.Middleware(mux)
|
||||
|
||||
// Create HTTP server
|
||||
a.server = &http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", a.config.Server.Host, a.config.Server.Port),
|
||||
Handler: handler,
|
||||
ReadTimeout: a.config.Server.ReadTimeout,
|
||||
WriteTimeout: a.config.Server.WriteTimeout,
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("addr", a.server.Addr).
|
||||
Msg("HTTP server configured")
|
||||
Str("addr", fmt.Sprintf("%s:%d", a.config.Server.Host, a.config.Server.Port)).
|
||||
Msg("Fiber server configured")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -320,13 +315,17 @@ func (a *App) Run() error {
|
||||
go a.rescanWorker.Start(ctx)
|
||||
}
|
||||
|
||||
// Start HTTP server in goroutine
|
||||
// Start download data aggregation worker (runs every hour)
|
||||
go a.startAggregationWorker(ctx)
|
||||
|
||||
// Start Fiber server in goroutine
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
addr := fmt.Sprintf("%s:%d", a.config.Server.Host, a.config.Server.Port)
|
||||
log.Info().
|
||||
Str("addr", a.server.Addr).
|
||||
Msg("Starting HTTP server")
|
||||
if err := a.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
Str("addr", addr).
|
||||
Msg("Starting Fiber server")
|
||||
if err := a.app.Listen(addr); err != nil {
|
||||
errChan <- err
|
||||
}
|
||||
}()
|
||||
@@ -352,12 +351,9 @@ func (a *App) Run() error {
|
||||
func (a *App) Shutdown() error {
|
||||
log.Info().Msg("Starting graceful shutdown")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Stop HTTP server
|
||||
if err := a.server.Shutdown(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("Error shutting down HTTP server")
|
||||
// Stop Fiber server
|
||||
if err := a.app.Shutdown(); err != nil {
|
||||
log.Error().Err(err).Msg("Error shutting down Fiber server")
|
||||
}
|
||||
|
||||
// Stop pre-warming worker
|
||||
@@ -391,3 +387,29 @@ func (a *App) Shutdown() error {
|
||||
log.Info().Msg("Shutdown complete")
|
||||
return nil
|
||||
}
|
||||
|
||||
// startAggregationWorker runs download data aggregation periodically
|
||||
func (a *App) startAggregationWorker(ctx context.Context) {
|
||||
log.Info().Msg("Starting download data aggregation worker (runs every hour)")
|
||||
|
||||
// Run immediately on startup
|
||||
if err := a.metadata.AggregateDownloadData(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to run initial download data aggregation")
|
||||
}
|
||||
|
||||
// Then run every hour
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Info().Msg("Aggregation worker stopped")
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := a.metadata.AggregateDownloadData(ctx); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to aggregate download data")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+184
-87
@@ -1,48 +1,45 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lukaszraczylo/gohoarder/internal/version"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/errors"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// handlePackages handles /api/packages endpoint
|
||||
func (a *App) handlePackages(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
func (a *App) handlePackages(c *fiber.Ctx) error {
|
||||
c.Set("Content-Type", "application/json")
|
||||
c.Set("Access-Control-Allow-Origin", "*")
|
||||
c.Set("Access-Control-Allow-Methods", "GET, DELETE, OPTIONS")
|
||||
c.Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
if c.Method() == "OPTIONS" {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
// Check if this is a vulnerability endpoint request
|
||||
if strings.HasSuffix(r.URL.Path, "/vulnerabilities") {
|
||||
a.handleVulnerabilities(w, r)
|
||||
return
|
||||
if strings.HasSuffix(c.Path(), "/vulnerabilities") {
|
||||
return a.handleVulnerabilities(c)
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
switch c.Method() {
|
||||
case "GET":
|
||||
a.handleListPackages(w, r)
|
||||
return a.handleListPackages(c)
|
||||
case "DELETE":
|
||||
a.handleDeletePackage(w, r)
|
||||
return a.handleDeletePackage(c)
|
||||
default:
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("method not allowed"))
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
// handleListPackages returns list of cached packages
|
||||
func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
func (a *App) handleListPackages(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
// Get packages from metadata store
|
||||
allPackages, err := a.metadata.ListPackages(ctx, &metadata.ListOptions{
|
||||
@@ -51,19 +48,33 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list packages")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to list packages"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to list packages"})
|
||||
}
|
||||
|
||||
log.Debug().Int("total_packages_from_db", len(allPackages)).Msg("Retrieved packages from database")
|
||||
|
||||
// Filter, clean, and deduplicate packages
|
||||
seen := make(map[string]*metadata.Package)
|
||||
// Map stores both cleaned package and original name for scan lookups
|
||||
type packageEntry struct {
|
||||
pkg *metadata.Package
|
||||
originalName string
|
||||
}
|
||||
seen := make(map[string]*packageEntry)
|
||||
skippedCount := 0
|
||||
for _, pkg := range allPackages {
|
||||
// Skip metadata entries (npm metadata pages, pypi pages, etc.)
|
||||
if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
|
||||
skippedCount++
|
||||
log.Debug().
|
||||
Str("name", pkg.Name).
|
||||
Str("version", pkg.Version).
|
||||
Str("registry", pkg.Registry).
|
||||
Msg("Skipping metadata entry")
|
||||
continue
|
||||
}
|
||||
|
||||
// Clean the package name (remove /@v/version.ext suffix)
|
||||
originalName := pkg.Name
|
||||
cleanName := pkg.Name
|
||||
if idx := strings.Index(cleanName, "/@v/"); idx != -1 {
|
||||
cleanName = cleanName[:idx]
|
||||
@@ -73,25 +84,41 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
||||
key := cleanName + "@" + pkg.Version
|
||||
|
||||
// Keep the entry with the largest size (typically .zip files)
|
||||
if existing, ok := seen[key]; !ok || pkg.Size > existing.Size {
|
||||
if existing, ok := seen[key]; !ok || pkg.Size > existing.pkg.Size {
|
||||
// Create a copy with cleaned name
|
||||
cleanPkg := *pkg
|
||||
cleanPkg.Name = cleanName
|
||||
seen[key] = &cleanPkg
|
||||
seen[key] = &packageEntry{
|
||||
pkg: &cleanPkg,
|
||||
originalName: originalName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to slice
|
||||
packages := make([]*metadata.Package, 0, len(seen))
|
||||
for _, pkg := range seen {
|
||||
packages = append(packages, pkg)
|
||||
log.Debug().
|
||||
Int("skipped_metadata", skippedCount).
|
||||
Int("unique_packages", len(seen)).
|
||||
Msg("Filtered and deduplicated packages")
|
||||
|
||||
// Convert map to slice, keeping track of original names
|
||||
type packageWithOriginalName struct {
|
||||
pkg *metadata.Package
|
||||
originalName string
|
||||
}
|
||||
packagesWithNames := make([]packageWithOriginalName, 0, len(seen))
|
||||
for _, entry := range seen {
|
||||
packagesWithNames = append(packagesWithNames, packageWithOriginalName{
|
||||
pkg: entry.pkg,
|
||||
originalName: entry.originalName,
|
||||
})
|
||||
}
|
||||
|
||||
// Enhance packages with vulnerability information if security scanning is enabled
|
||||
var response map[string]interface{}
|
||||
if a.config.Security.Enabled {
|
||||
enhancedPackages := make([]map[string]interface{}, 0, len(packages))
|
||||
for _, pkg := range packages {
|
||||
enhancedPackages := make([]map[string]interface{}, 0, len(packagesWithNames))
|
||||
for _, entry := range packagesWithNames {
|
||||
pkg := entry.pkg
|
||||
pkgMap := map[string]interface{}{
|
||||
"id": pkg.ID,
|
||||
"registry": pkg.Registry,
|
||||
@@ -106,7 +133,8 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Add vulnerability info if scanned
|
||||
if pkg.SecurityScanned {
|
||||
scanResult, err := a.metadata.GetScanResult(ctx, pkg.Registry, pkg.Name, pkg.Version)
|
||||
// Use original name for scan result lookup (handles Go packages with /@v/ suffix)
|
||||
scanResult, err := a.metadata.GetScanResult(ctx, pkg.Registry, entry.originalName, pkg.Version)
|
||||
if err == nil && scanResult != nil {
|
||||
// Count vulnerabilities by severity
|
||||
severityCounts := make(map[string]int)
|
||||
@@ -115,8 +143,8 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
pkgMap["vulnerabilities"] = map[string]interface{}{
|
||||
"scanned": true,
|
||||
"status": scanResult.Status,
|
||||
"scanned": true,
|
||||
"status": scanResult.Status,
|
||||
"scannedAt": scanResult.ScannedAt.Format(time.RFC3339),
|
||||
"counts": map[string]int{
|
||||
"critical": severityCounts["CRITICAL"],
|
||||
@@ -147,6 +175,11 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
||||
"total": len(enhancedPackages),
|
||||
}
|
||||
} else {
|
||||
// Non-enhanced mode - just return the packages
|
||||
packages := make([]*metadata.Package, 0, len(packagesWithNames))
|
||||
for _, entry := range packagesWithNames {
|
||||
packages = append(packages, entry.pkg)
|
||||
}
|
||||
response = map[string]interface{}{
|
||||
"packages": packages,
|
||||
"total": len(packages),
|
||||
@@ -154,21 +187,22 @@ func (a *App) handleListPackages(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Success response
|
||||
errors.WriteJSONSimple(w, http.StatusOK, response)
|
||||
return c.Status(fiber.StatusOK).JSON(response)
|
||||
}
|
||||
|
||||
// handleDeletePackage deletes a cached package
|
||||
func (a *App) handleDeletePackage(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
func (a *App) handleDeletePackage(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
// Parse path: /api/packages/{registry}/{name}/{version}
|
||||
// For Go packages, name can contain slashes (e.g., github.com/user/repo)
|
||||
// Version is always the last segment
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/packages/")
|
||||
path := strings.TrimPrefix(c.Path(), "/api/packages/")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 3 {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("invalid path format, expected /api/packages/{registry}/{name}/{version}"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "invalid path format, expected /api/packages/{registry}/{name}/{version}",
|
||||
})
|
||||
}
|
||||
|
||||
registry := parts[0]
|
||||
@@ -187,8 +221,7 @@ func (a *App) handleDeletePackage(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list packages for deletion")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to list packages"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to list packages"})
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
@@ -242,13 +275,11 @@ func (a *App) handleDeletePackage(w http.ResponseWriter, r *http.Request) {
|
||||
Msg("Delete operation completed")
|
||||
|
||||
if deletedCount == 0 {
|
||||
errors.WriteErrorSimple(w, errors.NotFound("package not found"))
|
||||
return
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "package not found"})
|
||||
}
|
||||
|
||||
if lastErr != nil && deletedCount == 0 {
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to delete package"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to delete package"})
|
||||
}
|
||||
} else {
|
||||
// For NPM and PyPI, delete directly
|
||||
@@ -259,8 +290,7 @@ func (a *App) handleDeletePackage(w http.ResponseWriter, r *http.Request) {
|
||||
Str("name", name).
|
||||
Str("version", version).
|
||||
Msg("Failed to delete package")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to delete package"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to delete package"})
|
||||
}
|
||||
deletedCount = 1
|
||||
}
|
||||
@@ -287,46 +317,41 @@ func (a *App) handleDeletePackage(w http.ResponseWriter, r *http.Request) {
|
||||
response["deleted_count"] = deletedCount
|
||||
}
|
||||
|
||||
errors.WriteJSONSimple(w, http.StatusOK, response)
|
||||
return c.Status(fiber.StatusOK).JSON(response)
|
||||
}
|
||||
|
||||
// handleStats handles /api/stats endpoint
|
||||
func (a *App) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
func (a *App) handleStats(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 r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
if c.Method() == "OPTIONS" {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("method not allowed"))
|
||||
return
|
||||
if c.Method() != "GET" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx := c.Context()
|
||||
|
||||
// Get cache statistics for all registries
|
||||
// Get cache statistics for all registries from database
|
||||
cacheStats, err := a.cache.GetStats(ctx, "")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to get cache stats")
|
||||
cacheStats = &metadata.Stats{}
|
||||
}
|
||||
|
||||
// Get all packages to calculate total size and downloads
|
||||
// Get all packages to calculate per-registry breakdown
|
||||
packages, err := a.metadata.ListPackages(ctx, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list packages")
|
||||
packages = []*metadata.Package{}
|
||||
}
|
||||
|
||||
// Calculate totals and registry breakdown from actual packages (exclude metadata entries like "list", "latest")
|
||||
var totalSize int64
|
||||
var totalDownloads int64
|
||||
var actualPackageCount int
|
||||
// Calculate per-registry breakdown (exclude metadata entries like "list", "latest")
|
||||
registryStats := make(map[string]map[string]interface{})
|
||||
|
||||
for _, pkg := range packages {
|
||||
@@ -334,9 +359,6 @@ func (a *App) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
if pkg.Version == "list" || pkg.Version == "latest" || pkg.Version == "metadata" || pkg.Version == "page" {
|
||||
continue
|
||||
}
|
||||
totalSize += pkg.Size
|
||||
totalDownloads += int64(pkg.DownloadCount)
|
||||
actualPackageCount++
|
||||
|
||||
// Track per-registry stats
|
||||
if _, ok := registryStats[pkg.Registry]; !ok {
|
||||
@@ -351,11 +373,11 @@ func (a *App) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
registryStats[pkg.Registry]["downloads"] = registryStats[pkg.Registry]["downloads"].(int64) + int64(pkg.DownloadCount)
|
||||
}
|
||||
|
||||
// Combine statistics
|
||||
// Combine statistics using database stats for accuracy
|
||||
stats := map[string]interface{}{
|
||||
"total_packages": actualPackageCount,
|
||||
"total_downloads": totalDownloads,
|
||||
"total_size": totalSize,
|
||||
"total_packages": cacheStats.TotalPackages,
|
||||
"total_downloads": cacheStats.TotalDownloads,
|
||||
"total_size": cacheStats.TotalSize,
|
||||
"cache_hits": cacheStats.TotalDownloads,
|
||||
"cache_misses": 0, // TODO: Track cache misses
|
||||
"cache_evictions": 0, // TODO: Track evictions
|
||||
@@ -370,27 +392,102 @@ func (a *App) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||
registries[registry] = regStats
|
||||
}
|
||||
|
||||
errors.WriteJSONSimple(w, http.StatusOK, map[string]interface{}{
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"stats": stats,
|
||||
"registries": registries,
|
||||
})
|
||||
}
|
||||
|
||||
// handleInfo handles /api/info endpoint
|
||||
func (a *App) handleInfo(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
// handleTimeSeriesStats handles /api/stats/timeseries endpoint
|
||||
// Returns time-series download statistics for charts
|
||||
func (a *App) handleTimeSeriesStats(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 r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
if c.Method() == "OPTIONS" {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("method not allowed"))
|
||||
return
|
||||
if c.Method() != "GET" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
// Get query parameters
|
||||
period := c.Query("period", "1day") // Default to 1 day
|
||||
registry := c.Query("registry") // Optional registry filter
|
||||
|
||||
// Validate period
|
||||
validPeriods := map[string]bool{"1h": true, "1day": true, "7day": true, "30day": true}
|
||||
if !validPeriods[period] {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "invalid period, must be one of: 1h, 1day, 7day, 30day",
|
||||
})
|
||||
}
|
||||
|
||||
// Get time-series stats
|
||||
stats, err := a.metadata.GetTimeSeriesStats(ctx, period, registry)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("period", period).Str("registry", registry).Msg("Failed to get time-series stats")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
||||
"error": "failed to get time-series statistics",
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(stats)
|
||||
}
|
||||
|
||||
// handleConfig handles /api/config endpoint
|
||||
// Returns runtime configuration for the frontend
|
||||
func (a *App) handleConfig(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"})
|
||||
}
|
||||
|
||||
// Build server URL from request
|
||||
scheme := "http"
|
||||
if c.Protocol() == "https" {
|
||||
scheme = "https"
|
||||
}
|
||||
serverURL := scheme + "://" + c.Hostname()
|
||||
|
||||
config := map[string]interface{}{
|
||||
"server_url": serverURL,
|
||||
"version": version.Version,
|
||||
"features": map[string]bool{
|
||||
"security_scanning": a.config.Security.Enabled,
|
||||
"websockets": true,
|
||||
},
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(config)
|
||||
}
|
||||
|
||||
// handleInfo handles /api/info endpoint
|
||||
func (a *App) handleInfo(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"})
|
||||
}
|
||||
|
||||
info := map[string]interface{}{
|
||||
@@ -411,5 +508,5 @@ func (a *App) handleInfo(w http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
}
|
||||
|
||||
errors.WriteJSONSimple(w, http.StatusOK, info)
|
||||
return c.Status(fiber.StatusOK).JSON(info)
|
||||
}
|
||||
|
||||
+111
-166
@@ -1,111 +1,94 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/auth"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/errors"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// requireAdmin middleware checks for admin authentication
|
||||
func (a *App) requireAdmin(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get API key from Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
errors.WriteErrorSimple(w, errors.New(errors.ErrCodeUnauthorized, "missing authorization header"))
|
||||
return
|
||||
}
|
||||
|
||||
// Extract bearer token
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
errors.WriteErrorSimple(w, errors.New(errors.ErrCodeUnauthorized, "invalid authorization header format, expected: Bearer <token>"))
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := parts[1]
|
||||
|
||||
// Validate API key
|
||||
key, err := a.authManager.ValidateAPIKey(r.Context(), apiKey)
|
||||
if err != nil {
|
||||
errors.WriteErrorSimple(w, errors.New(errors.ErrCodeUnauthorized, "invalid or expired API key"))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user has admin role or bypass management permission
|
||||
if key.Role != auth.RoleAdmin && !key.HasPermission(auth.PermissionManageBypasses) {
|
||||
errors.WriteErrorSimple(w, errors.New(errors.ErrCodeForbidden, "insufficient permissions, admin role required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Store user info in request context for handlers to use
|
||||
// For now, we'll just proceed - could enhance with context.WithValue
|
||||
next(w, r)
|
||||
func (a *App) requireAdmin(c *fiber.Ctx) error {
|
||||
// Get API key from Authorization header
|
||||
authHeader := c.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "missing authorization header",
|
||||
})
|
||||
}
|
||||
|
||||
// Extract bearer token
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid authorization header format, expected: Bearer <token>",
|
||||
})
|
||||
}
|
||||
|
||||
apiKey := parts[1]
|
||||
|
||||
// Validate API key
|
||||
key, err := a.authManager.ValidateAPIKey(c.Context(), apiKey)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
||||
"error": "invalid or expired API key",
|
||||
})
|
||||
}
|
||||
|
||||
// Check if user has admin role or bypass management permission
|
||||
if key.Role != auth.RoleAdmin && !key.HasPermission(auth.PermissionManageBypasses) {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
||||
"error": "insufficient permissions, admin role required",
|
||||
})
|
||||
}
|
||||
|
||||
// Continue to next handler
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
// handleAdminBypasses handles /api/admin/bypasses endpoint
|
||||
func (a *App) handleAdminBypasses(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
func (a *App) handleAdminBypasses(c *fiber.Ctx) error {
|
||||
c.Set("Content-Type", "application/json")
|
||||
c.Set("Access-Control-Allow-Origin", "*")
|
||||
c.Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
|
||||
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
if c.Method() == "OPTIONS" {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
// Check if there's an ID parameter
|
||||
id := c.Params("id")
|
||||
|
||||
switch c.Method() {
|
||||
case "GET":
|
||||
a.requireAdmin(a.handleListBypasses)(w, r)
|
||||
if id != "" {
|
||||
return a.handleGetBypass(c)
|
||||
}
|
||||
return a.handleListBypasses(c)
|
||||
case "POST":
|
||||
a.requireAdmin(a.handleCreateBypass)(w, r)
|
||||
default:
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("method not allowed"))
|
||||
}
|
||||
}
|
||||
|
||||
// handleBypassByID handles /api/admin/bypasses/{id} endpoint
|
||||
func (a *App) handleBypassByID(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, DELETE, PATCH, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
a.requireAdmin(a.handleGetBypass)(w, r)
|
||||
case "DELETE":
|
||||
a.requireAdmin(a.handleDeleteBypass)(w, r)
|
||||
return a.handleCreateBypass(c)
|
||||
case "PATCH":
|
||||
a.requireAdmin(a.handleUpdateBypass)(w, r)
|
||||
return a.handleUpdateBypass(c)
|
||||
case "DELETE":
|
||||
return a.handleDeleteBypass(c)
|
||||
default:
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("method not allowed"))
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
|
||||
}
|
||||
}
|
||||
|
||||
// handleListBypasses lists all CVE bypasses
|
||||
func (a *App) handleListBypasses(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
func (a *App) handleListBypasses(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
// Parse query parameters
|
||||
includeExpired := r.URL.Query().Get("include_expired") == "true"
|
||||
activeOnly := r.URL.Query().Get("active_only") == "true"
|
||||
bypassType := metadata.BypassType(r.URL.Query().Get("type"))
|
||||
includeExpired := c.Query("include_expired") == "true"
|
||||
activeOnly := c.Query("active_only") == "true"
|
||||
bypassType := metadata.BypassType(c.Query("type"))
|
||||
|
||||
opts := &metadata.BypassListOptions{
|
||||
IncludeExpired: includeExpired,
|
||||
@@ -116,11 +99,10 @@ func (a *App) handleListBypasses(w http.ResponseWriter, r *http.Request) {
|
||||
bypasses, err := a.metadata.ListCVEBypasses(ctx, opts)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list CVE bypasses")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to list bypasses"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to list bypasses"})
|
||||
}
|
||||
|
||||
errors.WriteJSONSimple(w, http.StatusOK, map[string]interface{}{
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"bypasses": bypasses,
|
||||
"total": len(bypasses),
|
||||
})
|
||||
@@ -128,57 +110,43 @@ func (a *App) handleListBypasses(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// CreateBypassRequest represents the request body for creating a bypass
|
||||
type CreateBypassRequest struct {
|
||||
Type metadata.BypassType `json:"type"` // "cve" or "package"
|
||||
Target string `json:"target"` // CVE ID or package name
|
||||
Reason string `json:"reason"` // Why this bypass is needed
|
||||
CreatedBy string `json:"created_by"` // Admin username
|
||||
ExpiresInHours int `json:"expires_in_hours"` // How many hours until expiration
|
||||
AppliesTo string `json:"applies_to,omitempty"` // Optional: limit CVE bypass to specific package
|
||||
NotifyOnExpiry bool `json:"notify_on_expiry"` // Send notification when expired
|
||||
Type metadata.BypassType `json:"type"` // "cve" or "package"
|
||||
Target string `json:"target"` // CVE ID or package name
|
||||
Reason string `json:"reason"` // Why this bypass is needed
|
||||
CreatedBy string `json:"created_by"` // Admin username
|
||||
ExpiresInHours int `json:"expires_in_hours"` // How many hours until expiration
|
||||
AppliesTo string `json:"applies_to,omitempty"` // Optional: limit CVE bypass to specific package
|
||||
NotifyOnExpiry bool `json:"notify_on_expiry"` // Send notification when expired
|
||||
}
|
||||
|
||||
// handleCreateBypass creates a new CVE bypass
|
||||
func (a *App) handleCreateBypass(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Parse request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("failed to read request body"))
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
func (a *App) handleCreateBypass(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
var req CreateBypassRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("invalid JSON in request body"))
|
||||
return
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON in request body"})
|
||||
}
|
||||
|
||||
// Validate request
|
||||
if req.Type != metadata.BypassTypeCVE && req.Type != metadata.BypassTypePackage {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("type must be 'cve' or 'package'"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be 'cve' or 'package'"})
|
||||
}
|
||||
|
||||
if req.Target == "" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("target is required"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "target is required"})
|
||||
}
|
||||
|
||||
if req.Reason == "" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("reason is required"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "reason is required"})
|
||||
}
|
||||
|
||||
if req.CreatedBy == "" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("created_by is required"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "created_by is required"})
|
||||
}
|
||||
|
||||
if req.ExpiresInHours <= 0 {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("expires_in_hours must be greater than 0"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "expires_in_hours must be greater than 0"})
|
||||
}
|
||||
|
||||
// Create bypass
|
||||
@@ -201,8 +169,7 @@ func (a *App) handleCreateBypass(w http.ResponseWriter, r *http.Request) {
|
||||
// Save to database
|
||||
if err := a.metadata.SaveCVEBypass(ctx, bypass); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to save CVE bypass")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to create bypass"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to create bypass"})
|
||||
}
|
||||
|
||||
log.Info().
|
||||
@@ -213,23 +180,21 @@ func (a *App) handleCreateBypass(w http.ResponseWriter, r *http.Request) {
|
||||
Time("expires_at", bypass.ExpiresAt).
|
||||
Msg("CVE bypass created")
|
||||
|
||||
errors.WriteJSONSimple(w, http.StatusCreated, map[string]interface{}{
|
||||
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
||||
"bypass": bypass,
|
||||
"message": "Bypass created successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// handleGetBypass gets a specific bypass by ID
|
||||
func (a *App) handleGetBypass(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
func (a *App) handleGetBypass(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
// Extract ID from path
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/admin/bypasses/")
|
||||
bypassID := path
|
||||
// Extract ID from parameter
|
||||
bypassID := c.Params("id")
|
||||
|
||||
if bypassID == "" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("bypass ID is required"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bypass ID is required"})
|
||||
}
|
||||
|
||||
// Get all bypasses and find the one with matching ID
|
||||
@@ -238,20 +203,18 @@ func (a *App) handleGetBypass(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list bypasses")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to get bypass"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to get bypass"})
|
||||
}
|
||||
|
||||
for _, bypass := range bypasses {
|
||||
if bypass.ID == bypassID {
|
||||
errors.WriteJSONSimple(w, http.StatusOK, map[string]interface{}{
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"bypass": bypass,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
errors.WriteErrorSimple(w, errors.NotFound("bypass not found"))
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bypass not found"})
|
||||
}
|
||||
|
||||
// UpdateBypassRequest represents the request body for updating a bypass
|
||||
@@ -262,30 +225,19 @@ type UpdateBypassRequest struct {
|
||||
}
|
||||
|
||||
// handleUpdateBypass updates a bypass (activate/deactivate or extend expiration)
|
||||
func (a *App) handleUpdateBypass(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
func (a *App) handleUpdateBypass(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
// Extract ID from path
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/admin/bypasses/")
|
||||
bypassID := path
|
||||
// Extract ID from parameter
|
||||
bypassID := c.Params("id")
|
||||
|
||||
if bypassID == "" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("bypass ID is required"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bypass ID is required"})
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("failed to read request body"))
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
var req UpdateBypassRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("invalid JSON in request body"))
|
||||
return
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON in request body"})
|
||||
}
|
||||
|
||||
// Get current bypass
|
||||
@@ -294,8 +246,7 @@ func (a *App) handleUpdateBypass(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to list bypasses")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to get bypass"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to get bypass"})
|
||||
}
|
||||
|
||||
var currentBypass *metadata.CVEBypass
|
||||
@@ -307,8 +258,7 @@ func (a *App) handleUpdateBypass(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if currentBypass == nil {
|
||||
errors.WriteErrorSimple(w, errors.NotFound("bypass not found"))
|
||||
return
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bypass not found"})
|
||||
}
|
||||
|
||||
// Update fields
|
||||
@@ -327,8 +277,7 @@ func (a *App) handleUpdateBypass(w http.ResponseWriter, r *http.Request) {
|
||||
// Save updated bypass
|
||||
if err := a.metadata.SaveCVEBypass(ctx, currentBypass); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to update bypass")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to update bypass"))
|
||||
return
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to update bypass"})
|
||||
}
|
||||
|
||||
log.Info().
|
||||
@@ -336,43 +285,39 @@ func (a *App) handleUpdateBypass(w http.ResponseWriter, r *http.Request) {
|
||||
Bool("active", currentBypass.Active).
|
||||
Msg("CVE bypass updated")
|
||||
|
||||
errors.WriteJSONSimple(w, http.StatusOK, map[string]interface{}{
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"bypass": currentBypass,
|
||||
"message": "Bypass updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// handleDeleteBypass deletes a bypass
|
||||
func (a *App) handleDeleteBypass(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
func (a *App) handleDeleteBypass(c *fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
|
||||
// Extract ID from path
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/admin/bypasses/")
|
||||
bypassID := path
|
||||
// Extract ID from parameter
|
||||
bypassID := c.Params("id")
|
||||
|
||||
if bypassID == "" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("bypass ID is required"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bypass ID is required"})
|
||||
}
|
||||
|
||||
// Delete bypass
|
||||
if err := a.metadata.DeleteCVEBypass(ctx, bypassID); err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
errors.WriteErrorSimple(w, errors.NotFound("bypass not found"))
|
||||
} else {
|
||||
log.Error().Err(err).Msg("Failed to delete bypass")
|
||||
errors.WriteErrorSimple(w, errors.InternalServer("failed to delete bypass"))
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bypass not found"})
|
||||
}
|
||||
return
|
||||
log.Error().Err(err).Msg("Failed to delete bypass")
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to delete bypass"})
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("bypass_id", bypassID).
|
||||
Msg("CVE bypass deleted")
|
||||
|
||||
errors.WriteJSONSimple(w, http.StatusOK, map[string]interface{}{
|
||||
"deleted": true,
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"deleted": true,
|
||||
"bypass_id": bypassID,
|
||||
"message": "Bypass deleted successfully",
|
||||
"message": "Bypass deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,40 +1,38 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/errors"
|
||||
"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(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
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 r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
if c.Method() == "OPTIONS" {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("method not allowed"))
|
||||
return
|
||||
if c.Method() != "GET" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
ctx := c.Context()
|
||||
|
||||
// Parse path: /api/packages/{registry}/{name}/{version}/vulnerabilities
|
||||
path := strings.TrimPrefix(r.URL.Path, "/api/packages/")
|
||||
path := strings.TrimPrefix(c.Path(), "/api/packages/")
|
||||
path = strings.TrimSuffix(path, "/vulnerabilities")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 3 {
|
||||
errors.WriteErrorSimple(w, errors.BadRequest("invalid path format, expected /api/packages/{registry}/{name}/{version}/vulnerabilities"))
|
||||
return
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
|
||||
"error": "invalid path format, expected /api/packages/{registry}/{name}/{version}/vulnerabilities",
|
||||
})
|
||||
}
|
||||
|
||||
registry := parts[0]
|
||||
@@ -53,13 +51,12 @@ func (a *App) handleVulnerabilities(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if package exists
|
||||
pkg, pkgErr := a.metadata.GetPackage(ctx, registry, name, version)
|
||||
if pkgErr != nil {
|
||||
errors.WriteErrorSimple(w, errors.NotFound("package not found"))
|
||||
return
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "package not found"})
|
||||
}
|
||||
|
||||
// Package exists but not scanned yet
|
||||
errors.WriteJSONSimple(w, http.StatusOK, map[string]interface{}{
|
||||
"package": map[string]string{
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"package": fiber.Map{
|
||||
"registry": registry,
|
||||
"name": name,
|
||||
"version": version,
|
||||
@@ -71,7 +68,6 @@ func (a *App) handleVulnerabilities(w http.ResponseWriter, r *http.Request) {
|
||||
"message": "Package not yet scanned for vulnerabilities",
|
||||
"security_scanned": pkg.SecurityScanned,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Get active bypasses to show which vulnerabilities are bypassed
|
||||
@@ -135,8 +131,8 @@ func (a *App) handleVulnerabilities(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Build response
|
||||
response := map[string]interface{}{
|
||||
"package": map[string]string{
|
||||
response := fiber.Map{
|
||||
"package": fiber.Map{
|
||||
"registry": registry,
|
||||
"name": name,
|
||||
"version": version,
|
||||
@@ -147,7 +143,7 @@ func (a *App) handleVulnerabilities(w http.ResponseWriter, r *http.Request) {
|
||||
"status": scanResult.Status,
|
||||
"vulnerabilities": enrichedVulns,
|
||||
"vulnerability_count": scanResult.VulnerabilityCount,
|
||||
"severity_counts": map[string]int{
|
||||
"severity_counts": fiber.Map{
|
||||
"critical": severityCounts["CRITICAL"],
|
||||
"high": severityCounts["HIGH"],
|
||||
"moderate": severityCounts["MODERATE"],
|
||||
@@ -156,5 +152,5 @@ func (a *App) handleVulnerabilities(w http.ResponseWriter, r *http.Request) {
|
||||
"bypassed_count": len(scanResult.Vulnerabilities) - (severityCounts["CRITICAL"] + severityCounts["HIGH"] + severityCounts["MODERATE"] + severityCounts["LOW"]),
|
||||
}
|
||||
|
||||
errors.WriteJSONSimple(w, http.StatusOK, response)
|
||||
return c.Status(fiber.StatusOK).JSON(response)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user