package server import ( "net/http" "time" "jarl/internal/auth" "jarl/internal/engine" "jarl/internal/model" "jarl/internal/stats" "jarl/internal/validate" ) // exposureFinding is an exposure problem with the fix Jarl can stage for // it ("" when there is none). type exposureFinding struct { validate.Problem Fix string `json:"fix,omitempty"` } type exposureScope struct { validate.Exposure RateLimit string `json:"rate_limit"` // off, log-only or enforcing Findings []exposureFinding `json:"findings"` } type exposureReport struct { Scopes []exposureScope `json:"scopes"` // Server holds server-wide findings (version, host name, management // interfaces). Server []exposureFinding `json:"server"` // RateLimiting is what rate limiting limited, or would have, in the // last 24 hours (with query statistics on). RateLimiting *rateLimitActivity `json:"rate_limiting,omitempty"` } type rateLimitActivity struct { Since string `json:"since"` Counts []stats.Entry `json:"counts"` // drop, slip, would-drop, would-slip Networks []stats.Entry `json:"networks"` // client networks, most limited first Names []stats.Entry `json:"names"` // "name TYPE" } // exposure: GET /exposure reports which views are reachable from the // internet and what the exposure checks found (docs/exposure.md). func (s *Server) exposure(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := require(p, auth.Action{Resource: auth.ResOptions}); err != nil { return err } st, err := s.store.LoadState(r.Context()) if err != nil { return err } problems := s.eng.Validate(st) out := exposureReport{Scopes: []exposureScope{}, Server: []exposureFinding{}} finding := func(pr validate.Problem, viewID int64) exposureFinding { _, desc := engine.ExposureFix(st, viewID, pr.Code) return exposureFinding{Problem: pr, Fix: desc} } scopes := map[string]int{} for _, x := range s.eng.Exposures(st) { sc := exposureScope{Exposure: x, RateLimit: "off", Findings: []exposureFinding{}} v := st.Views[x.ViewID] o := st.FindOption(model.ScopeOptions, 0, "rate-limit") if v != nil { if vo := st.FindOption(model.ScopeView, v.ID, "rate-limit"); vo != nil { o = vo } } if o != nil { sc.RateLimit = "enforcing" if validate.LogOnly(o) { sc.RateLimit = "log-only" } } scopes[x.View] = len(out.Scopes) out.Scopes = append(out.Scopes, sc) } for _, pr := range problems { if !validate.Ackable(pr.Code) { continue } if pr.ObjectType == "view" || pr.ObjectType == "zone" { if i, ok := scopes[pr.View]; ok { out.Scopes[i].Findings = append(out.Scopes[i].Findings, finding(pr, out.Scopes[i].ViewID)) continue } } out.Server = append(out.Server, finding(pr, 0)) } if s.statsQ != nil && p.Can(auth.Action{Resource: auth.ResStats}) { ctx, since := r.Context(), 24*time.Hour a := &rateLimitActivity{Since: "24h"} a.Counts, _ = s.statsQ.Breakdown(ctx, "rrl", since) a.Networks, _ = s.statsQ.Top(ctx, stats.KindRateLimited, since, 10) a.Names, _ = s.statsQ.Top(ctx, stats.KindRateLimitedName, since, 10) out.RateLimiting = a } writeJSON(w, http.StatusOK, out) return nil } // exposureFix: POST /exposure/fix {"view_id": 3, "code": "..."} stages (or // applies) the fix for a finding, like any other change. func (s *Server) exposureFix(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { var in struct { ViewID int64 `json:"view_id"` View string `json:"view"` Code string `json:"code"` } if err := decodeJSON(r, &in); err != nil { return err } st, err := s.store.LoadState(r.Context()) if err != nil { return err } if in.View != "" { v := st.ViewByName(in.View) if v == nil { return notFound("no view %s", in.View) } in.ViewID = v.ID } reqs, desc := engine.ExposureFix(st, in.ViewID, in.Code) if reqs == nil { return badRequest("there is no automatic fix for %s: what to allow is yours to decide; change the option yourself, or acknowledge the finding", in.Code) } return s.mutate(w, r, p, desc, reqs, nil) } // exposureAck: POST /exposure/ack {"view": "external", "code": "...", // "reason": "..."} accepts a finding (unack takes it back). func (s *Server) exposureAck(ack bool) handler { return func(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { var in engine.AckInput if err := decodeJSON(r, &in); err != nil { return err } op := engine.OpExposureUnack if ack { op = engine.OpExposureAck } return s.mutate(w, r, p, "", []engine.Request{engine.NewRequest(op, 0, in)}, nil) } }