180 lines
3.5 KiB
Go
180 lines
3.5 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
router.go
|
|
|
|
Chi-based HTTP router for the hive dashboard API
|
|
|
|
Mounts all REST endpoints and the WebSocket handler behind a
|
|
middleware chain of panic recovery, CORS, and request logging.
|
|
Implements the types.Service interface so the API server
|
|
participates in the errgroup-managed lifecycle alongside the
|
|
honeypot services.
|
|
*/
|
|
|
|
package api
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/rs/zerolog"
|
|
|
|
"github.com/CarterPerez-dev/hive/internal/config"
|
|
"github.com/CarterPerez-dev/hive/internal/event"
|
|
"github.com/CarterPerez-dev/hive/internal/mitre"
|
|
"github.com/CarterPerez-dev/hive/internal/session"
|
|
"github.com/CarterPerez-dev/hive/internal/store"
|
|
"github.com/CarterPerez-dev/hive/pkg/types"
|
|
)
|
|
|
|
const (
|
|
defaultPageLimit = 50
|
|
maxPageLimit = 500
|
|
defaultStatsDuration = 24 * time.Hour
|
|
defaultCredentialTop = 20
|
|
defaultAttackerTop = 50
|
|
corsMaxAgeSeconds = "86400"
|
|
wsEventBuffer = 256
|
|
)
|
|
|
|
type Server struct {
|
|
store store.Store
|
|
bus *event.Bus
|
|
mitreIdx *mitre.Index
|
|
tracker *session.Tracker
|
|
cfg *config.Config
|
|
logger *zerolog.Logger
|
|
router *chi.Mux
|
|
replayDir string
|
|
startedAt time.Time
|
|
}
|
|
|
|
func New(
|
|
db store.Store,
|
|
bus *event.Bus,
|
|
mitreIdx *mitre.Index,
|
|
tracker *session.Tracker,
|
|
cfg *config.Config,
|
|
logger *zerolog.Logger,
|
|
) *Server {
|
|
s := &Server{
|
|
store: db,
|
|
bus: bus,
|
|
mitreIdx: mitreIdx,
|
|
tracker: tracker,
|
|
cfg: cfg,
|
|
logger: logger,
|
|
replayDir: cfg.Log.ReplayDir,
|
|
startedAt: time.Now().UTC(),
|
|
}
|
|
|
|
s.router = s.buildRouter()
|
|
return s
|
|
}
|
|
|
|
func (s *Server) Name() string {
|
|
return "api"
|
|
}
|
|
|
|
func (s *Server) Start(ctx context.Context) error {
|
|
addr := s.cfg.Addr(s.cfg.API.Port)
|
|
|
|
srv := &http.Server{
|
|
Addr: addr,
|
|
Handler: s.router,
|
|
ReadTimeout: s.cfg.API.ReadTimeout,
|
|
WriteTimeout: s.cfg.API.WriteTimeout,
|
|
}
|
|
|
|
go func() {
|
|
<-ctx.Done()
|
|
shutdownCtx, cancel := context.WithTimeout(
|
|
context.Background(),
|
|
config.DefaultShutdownTimeout,
|
|
)
|
|
defer cancel()
|
|
_ = srv.Shutdown(shutdownCtx)
|
|
}()
|
|
|
|
s.logger.Info().
|
|
Str("addr", addr).
|
|
Msg("api server listening")
|
|
|
|
if err := srv.ListenAndServe(); err != nil &&
|
|
err != http.ErrServerClosed {
|
|
return fmt.Errorf("api server: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Server) buildRouter() *chi.Mux {
|
|
r := chi.NewRouter()
|
|
|
|
r.Use(recoverer(s.logger))
|
|
r.Use(corsMiddleware(s.cfg.API.CORSOrigins))
|
|
r.Use(requestLogger(s.logger))
|
|
|
|
r.Route("/api", func(r chi.Router) {
|
|
r.Get("/health", s.handleHealth)
|
|
|
|
r.Route("/stats", func(r chi.Router) {
|
|
r.Get("/overview", s.handleStatsOverview)
|
|
r.Get("/countries", s.handleStatsCountries)
|
|
r.Get(
|
|
"/credentials",
|
|
s.handleStatsCredentials,
|
|
)
|
|
})
|
|
|
|
r.Route("/events", func(r chi.Router) {
|
|
r.Get("/", s.handleEvents)
|
|
})
|
|
|
|
r.Route("/sessions", func(r chi.Router) {
|
|
r.Get("/", s.handleSessions)
|
|
r.Get("/{id}", s.handleSessionByID)
|
|
r.Get(
|
|
"/{id}/replay",
|
|
s.handleSessionReplay,
|
|
)
|
|
})
|
|
|
|
r.Route("/attackers", func(r chi.Router) {
|
|
r.Get("/", s.handleAttackers)
|
|
r.Get("/{id}", s.handleAttackerByID)
|
|
})
|
|
|
|
r.Route("/mitre", func(r chi.Router) {
|
|
r.Get(
|
|
"/techniques",
|
|
s.handleMITRETechniques,
|
|
)
|
|
r.Get("/heatmap", s.handleMITREHeatmap)
|
|
})
|
|
|
|
r.Route("/iocs", func(r chi.Router) {
|
|
r.Get("/", s.handleIOCs)
|
|
r.Get(
|
|
"/export/stix",
|
|
s.handleIOCExportSTIX,
|
|
)
|
|
r.Get(
|
|
"/export/blocklist",
|
|
s.handleIOCExportBlocklist,
|
|
)
|
|
})
|
|
|
|
r.Get("/sensors", s.handleSensors)
|
|
})
|
|
|
|
r.Get("/ws/events", s.handleWebSocket)
|
|
|
|
return r
|
|
}
|
|
|
|
var _ types.Service = (*Server)(nil)
|