mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
Emit POSTURE_CHECK_FAILED for Active Users with USER source, backfill user failures separately from devices, and stop logging SYNC on manual run.
422 lines
14 KiB
Go
422 lines
14 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/gorilla/mux"
|
|
"github.com/gravitl/netmaker/db"
|
|
dbtypes "github.com/gravitl/netmaker/db/types"
|
|
"github.com/gravitl/netmaker/logger"
|
|
"github.com/gravitl/netmaker/logic"
|
|
"github.com/gravitl/netmaker/middleware"
|
|
"github.com/gravitl/netmaker/models"
|
|
"github.com/gravitl/netmaker/mq"
|
|
proLogic "github.com/gravitl/netmaker/pro/logic"
|
|
"github.com/gravitl/netmaker/schema"
|
|
"github.com/gravitl/netmaker/scope"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func PostureCheckHandlers(r *mux.Router) {
|
|
r.HandleFunc("/api/v1/posture_check", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(createPostureCheck)))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/v1/posture_check", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(listPostureChecks)))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/v1/posture_check", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(updatePostureCheck)))).Methods(http.MethodPut)
|
|
r.HandleFunc("/api/v1/posture_check", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(deletePostureCheck)))).Methods(http.MethodDelete)
|
|
r.HandleFunc("/api/v1/posture_check/run", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(triggerPostureChecks)))).Methods(http.MethodPost)
|
|
r.HandleFunc("/api/v1/posture_check/attrs", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(listPostureChecksAttrs)))).Methods(http.MethodGet)
|
|
r.HandleFunc("/api/v1/posture_check/violations", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true, http.HandlerFunc(listPostureCheckViolatedNodes)))).Methods(http.MethodGet)
|
|
}
|
|
|
|
// @Summary List Posture Checks Available Attributes
|
|
// @Router /api/v1/posture_check/attrs [get]
|
|
// @Tags Posture Check
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func listPostureChecksAttrs(w http.ResponseWriter, r *http.Request) {
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, schema.PostureCheckAttrValues, "fetched posture checks")
|
|
}
|
|
|
|
// @Summary Create Posture Check
|
|
// @Router /api/v1/posture_check [post]
|
|
// @Tags Posture Check
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body schema.PostureCheck true "Posture Check payload"
|
|
// @Success 200 {object} schema.PostureCheck
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func createPostureCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var req schema.PostureCheck
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "error decoding request body: ",
|
|
err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
if err := proLogic.ValidatePostureCheck(r.Context(), &req); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
pc := schema.PostureCheck{
|
|
ID: uuid.New().String(),
|
|
TenantID: scope.ID(r.Context()),
|
|
Name: req.Name,
|
|
NetworkID: req.NetworkID,
|
|
Description: req.Description,
|
|
Tags: req.Tags,
|
|
UserGroups: req.UserGroups,
|
|
Attribute: req.Attribute,
|
|
Values: req.Values,
|
|
Config: req.Config,
|
|
Severity: req.Severity,
|
|
Status: true,
|
|
CreatedBy: r.Header.Get("user"),
|
|
CreatedAt: time.Now().UTC(),
|
|
}
|
|
err = pc.Create(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error creating posture check "+err.Error()), logic.Internal),
|
|
)
|
|
return
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Create,
|
|
Source: models.Subject{
|
|
ID: r.Header.Get("user"),
|
|
Name: r.Header.Get("user"),
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: r.Header.Get("user"),
|
|
Target: models.Subject{
|
|
ID: pc.ID,
|
|
Name: pc.Name,
|
|
Type: schema.PostureCheckSub,
|
|
},
|
|
NetworkID: schema.NetworkID(pc.NetworkID),
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go mq.PublishPeerUpdate(ctx, false)
|
|
go proLogic.RunPostureChecksForTenant(ctx)
|
|
proLogic.PopulatePostureCheckGroupNames(r.Context(), []schema.PostureCheck{pc})
|
|
logic.ReturnSuccessResponseWithJson(w, r, pc, "created posture check")
|
|
}
|
|
|
|
// @Summary List Posture Checks
|
|
// @Router /api/v1/posture_check [get]
|
|
// @Tags Posture Check
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param network query string true "Network ID"
|
|
// @Param id query string false "Posture Check ID to fetch a specific check"
|
|
// @Success 200 {array} schema.PostureCheck
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func listPostureChecks(w http.ResponseWriter, r *http.Request) {
|
|
|
|
network := r.URL.Query().Get("network")
|
|
if network == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), logic.BadReq))
|
|
return
|
|
}
|
|
err := (&schema.Network{Name: network}).Get(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network not found"), logic.BadReq))
|
|
return
|
|
}
|
|
id := r.URL.Query().Get("id")
|
|
if id != "" {
|
|
pc := schema.PostureCheck{ID: id}
|
|
err := pc.Get(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error listing posture checks "+err.Error()), "internal"),
|
|
)
|
|
return
|
|
}
|
|
proLogic.PopulatePostureCheckGroupNames(r.Context(), []schema.PostureCheck{pc})
|
|
logic.ReturnSuccessResponseWithJson(w, r, pc, "fetched posture check")
|
|
return
|
|
}
|
|
pc := schema.PostureCheck{NetworkID: schema.NetworkID(network)}
|
|
list, err := pc.ListByNetwork(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error listing posture checks "+err.Error()), "internal"),
|
|
)
|
|
return
|
|
}
|
|
proLogic.PopulatePostureCheckGroupNames(r.Context(), list)
|
|
logic.ReturnSuccessResponseWithJson(w, r, list, "fetched posture checks")
|
|
}
|
|
|
|
// @Summary Update Posture Check
|
|
// @Router /api/v1/posture_check [put]
|
|
// @Tags Posture Check
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param body body schema.PostureCheck true "Posture Check payload"
|
|
// @Success 200 {object} schema.PostureCheck
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func updatePostureCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
|
var updatePc schema.PostureCheck
|
|
err := json.NewDecoder(r.Body).Decode(&updatePc)
|
|
if err != nil {
|
|
logger.Log(0, "error decoding request body: ",
|
|
err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if err := proLogic.ValidatePostureCheck(r.Context(), &updatePc); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
pc := schema.PostureCheck{ID: updatePc.ID}
|
|
err = pc.Get(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
proLogic.MergePostureCheckUpdate(&pc, &updatePc)
|
|
if err := proLogic.ValidatePostureCheck(r.Context(), &updatePc); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
var updateStatus bool
|
|
if updatePc.Status != pc.Status {
|
|
updateStatus = true
|
|
}
|
|
event := &models.Event{
|
|
Action: schema.Update,
|
|
Source: models.Subject{
|
|
ID: r.Header.Get("user"),
|
|
Name: r.Header.Get("user"),
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: r.Header.Get("user"),
|
|
Target: models.Subject{
|
|
ID: pc.ID,
|
|
Name: updatePc.Name,
|
|
Type: schema.PostureCheckSub,
|
|
},
|
|
Diff: models.Diff{
|
|
Old: pc,
|
|
New: updatePc,
|
|
},
|
|
NetworkID: pc.NetworkID,
|
|
Origin: schema.Dashboard,
|
|
}
|
|
pc.Tags = updatePc.Tags
|
|
pc.UserGroups = updatePc.UserGroups
|
|
pc.Attribute = updatePc.Attribute
|
|
pc.Values = updatePc.Values
|
|
pc.Config = updatePc.Config
|
|
pc.Description = updatePc.Description
|
|
pc.Name = updatePc.Name
|
|
pc.Severity = updatePc.Severity
|
|
pc.Status = updatePc.Status
|
|
pc.UpdatedAt = time.Now().UTC()
|
|
|
|
err = pc.Update(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(
|
|
w,
|
|
r,
|
|
logic.FormatError(errors.New("error updating posture check "+err.Error()), "internal"),
|
|
)
|
|
return
|
|
}
|
|
if updateStatus {
|
|
pc.UpdateStatus(r.Context())
|
|
}
|
|
logic.LogEvent(r.Context(), event)
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go mq.PublishPeerUpdate(ctx, false)
|
|
go proLogic.RunPostureChecksForTenant(ctx)
|
|
proLogic.PopulatePostureCheckGroupNames(r.Context(), []schema.PostureCheck{pc})
|
|
logic.ReturnSuccessResponseWithJson(w, r, pc, "updated posture check")
|
|
}
|
|
|
|
// @Summary Delete Posture Check
|
|
// @Router /api/v1/posture_check [delete]
|
|
// @Tags Posture Check
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param id query string true "Posture Check ID"
|
|
// @Success 200 {object} schema.PostureCheck
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func deletePostureCheck(w http.ResponseWriter, r *http.Request) {
|
|
|
|
id := r.URL.Query().Get("id")
|
|
if id == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("id is required"), "badrequest"))
|
|
return
|
|
}
|
|
pc := schema.PostureCheck{ID: id}
|
|
err := pc.Get(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
err = pc.Delete(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
|
return
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.Delete,
|
|
Source: models.Subject{
|
|
ID: r.Header.Get("user"),
|
|
Name: r.Header.Get("user"),
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: r.Header.Get("user"),
|
|
Target: models.Subject{
|
|
ID: pc.ID,
|
|
Name: pc.Name,
|
|
Type: schema.PostureCheckSub,
|
|
},
|
|
NetworkID: schema.NetworkID(pc.NetworkID),
|
|
Origin: schema.Dashboard,
|
|
Diff: models.Diff{
|
|
Old: pc,
|
|
New: nil,
|
|
},
|
|
})
|
|
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go mq.PublishPeerUpdate(ctx, false)
|
|
go proLogic.RunPostureChecksForTenant(ctx)
|
|
logic.ReturnSuccessResponseWithJson(w, r, pc, "deleted posture check")
|
|
}
|
|
|
|
// @Summary Trigger an out-of-cycle posture check evaluation
|
|
// @Router /api/v1/posture_check/run [post]
|
|
// @Tags Posture Check
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Success 202 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
func triggerPostureChecks(w http.ResponseWriter, r *http.Request) {
|
|
if !logic.GetFeatureFlags(r.Context()).EnablePostureChecks {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("posture checks are not enabled on your plan"), logic.BadReq))
|
|
return
|
|
}
|
|
|
|
go func(ctx context.Context) {
|
|
if err := proLogic.RunPostureChecksForTenant(ctx); err != nil {
|
|
logger.Log(0, "posture check: manual run failed:", err.Error())
|
|
}
|
|
mq.PublishPeerUpdate(ctx, false)
|
|
}(scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context())))
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, map[string]any{"queued": true}, "posture checks queued")
|
|
}
|
|
|
|
// @Summary List Posture Check violated Nodes
|
|
// @Router /api/v1/posture_check/violations [get]
|
|
// @Tags Posture Check
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param network query string true "Network ID"
|
|
// @Param users query string false "If 'true', list violated users instead of nodes"
|
|
// @Success 200 {array} models.ApiNode
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 401 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func listPostureCheckViolatedNodes(w http.ResponseWriter, r *http.Request) {
|
|
|
|
networkName := r.URL.Query().Get("network")
|
|
if networkName == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), logic.BadReq))
|
|
return
|
|
}
|
|
listViolatedusers := r.URL.Query().Get("users") == "true"
|
|
violatedNodes := []models.Node{}
|
|
if listViolatedusers {
|
|
extclients, err := logic.GetNetworkExtClients(r.Context(), networkName)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
|
return
|
|
}
|
|
for _, extclient := range extclients {
|
|
if extclient.DeviceID != "" {
|
|
if len(extclient.PostureChecksViolations) > 0 {
|
|
violatedNodes = append(violatedNodes, models.ConvertToStaticNode(extclient))
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
network := &schema.Network{
|
|
Name: networkName,
|
|
}
|
|
err := network.Get(r.Context())
|
|
if err != nil {
|
|
errType := logic.Internal
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
errType = logic.BadReq
|
|
}
|
|
|
|
err = fmt.Errorf("failed to list posture check violated nodes in network %s: error fetching network: %v", networkName, err)
|
|
logger.Log(0, r.Header.Get("user"), err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, errType))
|
|
return
|
|
}
|
|
|
|
_nodes, err := (&schema.Node{}).ListAll(
|
|
r.Context(),
|
|
dbtypes.WithPreloads("Host"),
|
|
dbtypes.WithFilter("network_id", network.ID),
|
|
dbtypes.WithNotFilter("posture_check_severity", schema.SeverityUnknown),
|
|
)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
|
|
return
|
|
}
|
|
|
|
for _, _node := range _nodes {
|
|
_node.Network = network
|
|
node := logic.ConvertSchemaNodeToModelsNode(&_node)
|
|
violatedNodes = append(violatedNodes, *node)
|
|
}
|
|
}
|
|
apiNodes := logic.GetAllNodesAPI(violatedNodes)
|
|
logic.SortApiNodes(apiNodes[:])
|
|
logic.ReturnSuccessResponseWithJson(w, r, apiNodes, "fetched posture checks violated nodes")
|
|
}
|