mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
fix(posture): keep violation cycles consistent and audit new failures
Skip violation details on GetAllNodes, replace cycles transactionally, and emit POSTURE_CHECK_FAILED for new CheckIDs (with one-time backfill).
This commit is contained in:
@@ -271,6 +271,7 @@ func joinHostToNetworks(ctx context.Context, key models.EnrollmentKey, host *sch
|
||||
true,
|
||||
)
|
||||
if len(violations) > 0 {
|
||||
logic.EmitNewPostureViolationEvents(ctx, nil, violations, models.PostureCheckDeviceInfo{HostID: host.ID.String()}, schema.NetworkID(network.Name))
|
||||
logger.Log(0, fmt.Sprintf("skipping joining network %s due to violations", network.Name))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -538,6 +538,8 @@ func handleHostRegister(w http.ResponseWriter, r *http.Request) {
|
||||
violations, _ := logic.CheckPostureViolationsForHost(r.Context(), &newHost, keyTags, schema.NetworkID(netI), true)
|
||||
if len(violations) == 0 {
|
||||
joinNetworks = append(joinNetworks, netI)
|
||||
} else {
|
||||
logic.EmitNewPostureViolationEvents(r.Context(), nil, violations, models.PostureCheckDeviceInfo{HostID: newHost.ID.String()}, schema.NetworkID(netI))
|
||||
}
|
||||
}
|
||||
if len(joinNetworks) != len(enrollmentKey.Networks) && len(joinNetworks) == 0 {
|
||||
|
||||
@@ -646,8 +646,10 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
|
||||
if extclient.DeviceID != "" {
|
||||
// check for violations connecting from desktop app
|
||||
staticNode := models.ConvertToStaticNode(extclient)
|
||||
violations, _ := logic.CheckPostureViolations(r.Context(), logic.GetPostureCheckDeviceInfoByNode(r.Context(), &staticNode), schema.NetworkID(extclient.Network))
|
||||
deviceInfo := logic.GetPostureCheckDeviceInfoByNode(r.Context(), &staticNode)
|
||||
violations, _ := logic.CheckPostureViolations(r.Context(), deviceInfo, schema.NetworkID(extclient.Network))
|
||||
if len(violations) > 0 {
|
||||
logic.EmitNewPostureViolationEvents(r.Context(), nil, violations, deviceInfo, schema.NetworkID(extclient.Network))
|
||||
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("posture check violations"), logic.Forbidden))
|
||||
return
|
||||
}
|
||||
@@ -927,8 +929,10 @@ func updateExtClient(w http.ResponseWriter, r *http.Request) {
|
||||
if newclient.DeviceID != "" && newclient.Enabled {
|
||||
// check for violations connecting from desktop app
|
||||
staticNode := models.ConvertToStaticNode(newclient)
|
||||
violations, _ := logic.CheckPostureViolations(r.Context(), logic.GetPostureCheckDeviceInfoByNode(r.Context(), &staticNode), schema.NetworkID(newclient.Network))
|
||||
deviceInfo := logic.GetPostureCheckDeviceInfoByNode(r.Context(), &staticNode)
|
||||
violations, _ := logic.CheckPostureViolations(r.Context(), deviceInfo, schema.NetworkID(newclient.Network))
|
||||
if len(violations) > 0 {
|
||||
logic.EmitNewPostureViolationEvents(r.Context(), nil, violations, deviceInfo, schema.NetworkID(newclient.Network))
|
||||
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("posture check violations"), logic.Forbidden))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -679,7 +679,10 @@ func hostUpdateFallback(w http.ResponseWriter, r *http.Request) {
|
||||
)
|
||||
for _, _node := range _nodes {
|
||||
node := logic.ConvertSchemaNodeToModelsNode(&_node)
|
||||
node.PostureChecksViolations, node.PostureCheckViolationSeverityLevel = logic.CheckPostureViolations(ctx, logic.GetPostureCheckDeviceInfoByNode(ctx, node), schema.NetworkID(node.Network))
|
||||
deviceInfo := logic.GetPostureCheckDeviceInfoByNode(ctx, node)
|
||||
oldViolations := node.PostureChecksViolations
|
||||
node.PostureChecksViolations, node.PostureCheckViolationSeverityLevel = logic.CheckPostureViolations(ctx, deviceInfo, schema.NetworkID(node.Network))
|
||||
logic.EmitNewPostureViolationEvents(ctx, oldViolations, node.PostureChecksViolations, deviceInfo, schema.NetworkID(node.Network))
|
||||
_node.PostureCheckSeverity = node.PostureCheckViolationSeverityLevel
|
||||
_node.PostureCheckLastEvaluationCycleID = uuid.NewString()
|
||||
_node.PostureCheckLastEvaluatedAt = time.Now().UTC()
|
||||
@@ -1011,6 +1014,7 @@ func addHostToNetwork(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
violations, _ := logic.CheckPostureViolationsForHost(r.Context(), host, nil, schema.NetworkID(networkID), true)
|
||||
if len(violations) > 0 {
|
||||
logic.EmitNewPostureViolationEvents(r.Context(), nil, violations, models.PostureCheckDeviceInfo{HostID: host.ID.String()}, schema.NetworkID(networkID))
|
||||
logic.ReturnErrorResponseWithJson(w, r, violations, logic.FormatError(errors.New("posture check violations"), logic.BadReq))
|
||||
return
|
||||
}
|
||||
@@ -1913,6 +1917,7 @@ func approvePendingHost(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
violations, _ := logic.CheckPostureViolationsForHost(r.Context(), host, keyTags, schema.NetworkID(network.Name), true)
|
||||
if len(violations) > 0 {
|
||||
logic.EmitNewPostureViolationEvents(r.Context(), nil, violations, models.PostureCheckDeviceInfo{HostID: host.ID.String()}, schema.NetworkID(network.Name))
|
||||
err = fmt.Errorf("failed to approve pending host (%s): posture check violations", id)
|
||||
logger.Log(0, err.Error())
|
||||
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.BadReq))
|
||||
@@ -1981,6 +1986,7 @@ func addDefaultHostToNetworks(ctx context.Context, host *schema.Host) {
|
||||
|
||||
violations, _ := logic.CheckPostureViolationsForHost(ctx, host, make(map[models.TagID]struct{}), schema.NetworkID(network.Name), true)
|
||||
if len(violations) > 0 {
|
||||
logic.EmitNewPostureViolationEvents(ctx, nil, violations, models.PostureCheckDeviceInfo{HostID: host.ID.String()}, schema.NetworkID(network.Name))
|
||||
logger.Log(2, "skipping network", network.Name, "for default host", host.Name, ": posture check violations")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -44,6 +44,11 @@ var CheckPostureViolations = func(ctx context.Context, d models.PostureCheckDevi
|
||||
return []models.Violation{}, schema.SeverityUnknown
|
||||
}
|
||||
|
||||
// EmitNewPostureViolationEvents records audit events for newly observed posture
|
||||
// failures. No-op in community; wired by pro.
|
||||
var EmitNewPostureViolationEvents = func(ctx context.Context, oldVi, newVi []models.Violation, d models.PostureCheckDeviceInfo, network schema.NetworkID) {
|
||||
}
|
||||
|
||||
var CheckPostureViolationsForHost = func(ctx context.Context, host *schema.Host, tags map[models.TagID]struct{}, network schema.NetworkID, skipAutoUpdate bool) ([]models.Violation, schema.Severity) {
|
||||
if host == nil {
|
||||
return []models.Violation{}, schema.SeverityUnknown
|
||||
|
||||
+36
-8
@@ -328,8 +328,23 @@ func DeleteNodeByID(ctx context.Context, node *models.Node) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllNodes - returns all nodes in the DB
|
||||
// GetAllNodes - returns all nodes in the DB.
|
||||
// List/API responses use posture severity/cycle fields already on the node row
|
||||
// (written by the background posture hook). Full violation details are omitted
|
||||
// here — use GetAllNodesWithViolations or single-node convert / the dedicated
|
||||
// violations API when the detail array is required.
|
||||
func GetAllNodes(ctx context.Context) ([]models.Node, error) {
|
||||
return getAllNodes(ctx, false)
|
||||
}
|
||||
|
||||
// GetAllNodesWithViolations is like GetAllNodes but attaches current-cycle
|
||||
// posture_check_violations in one batch query. Intended for the posture hook
|
||||
// (event diffs), not for high-frequency list APIs.
|
||||
func GetAllNodesWithViolations(ctx context.Context) ([]models.Node, error) {
|
||||
return getAllNodes(ctx, true)
|
||||
}
|
||||
|
||||
func getAllNodes(ctx context.Context, withViolations bool) ([]models.Node, error) {
|
||||
var nodes []models.Node
|
||||
_nodes, err := (&schema.Node{}).ListAll(ctx, dbtypes.WithAllPreloads())
|
||||
if err != nil {
|
||||
@@ -341,7 +356,9 @@ func GetAllNodes(ctx context.Context) ([]models.Node, error) {
|
||||
ensureNodeMutex(node)
|
||||
nodes = append(nodes, *node)
|
||||
}
|
||||
attachPostureViolations(ctx, _nodes, nodes)
|
||||
if withViolations {
|
||||
attachPostureViolations(ctx, _nodes, nodes)
|
||||
}
|
||||
|
||||
return nodes, nil
|
||||
}
|
||||
@@ -349,6 +366,7 @@ func GetAllNodes(ctx context.Context) ([]models.Node, error) {
|
||||
// attachPostureViolations loads violations for all nodes in one query and
|
||||
// assigns each node's current-cycle violations onto the models.Node slice.
|
||||
// schemaNodes and modelsNodes must be the same length and aligned by index.
|
||||
// Used by GetAllNodesWithViolations (posture hook), not the high-frequency list API.
|
||||
func attachPostureViolations(ctx context.Context, schemaNodes []schema.Node, modelsNodes []models.Node) {
|
||||
if len(schemaNodes) == 0 || len(schemaNodes) != len(modelsNodes) {
|
||||
return
|
||||
@@ -367,7 +385,6 @@ func attachPostureViolations(ctx context.Context, schemaNodes []schema.Node, mod
|
||||
slog.Warn("failed to batch-load posture violations", "error", err, "nodes", len(ids))
|
||||
return
|
||||
}
|
||||
// nodeID -> cycleID -> violations
|
||||
byNodeCycle := make(map[string]map[string][]models.Violation, len(ids))
|
||||
for _, v := range all {
|
||||
cycles := byNodeCycle[v.NodeID]
|
||||
@@ -385,11 +402,22 @@ func attachPostureViolations(ctx context.Context, schemaNodes []schema.Node, mod
|
||||
}
|
||||
for i := range modelsNodes {
|
||||
cycleID := schemaNodes[i].PostureCheckLastEvaluationCycleID
|
||||
if cycleID == "" {
|
||||
cycles := byNodeCycle[schemaNodes[i].ID]
|
||||
if cycles == nil {
|
||||
continue
|
||||
}
|
||||
if cycles, ok := byNodeCycle[schemaNodes[i].ID]; ok {
|
||||
modelsNodes[i].PostureChecksViolations = cycles[cycleID]
|
||||
if cycleID != "" {
|
||||
if v, ok := cycles[cycleID]; ok {
|
||||
modelsNodes[i].PostureChecksViolations = v
|
||||
continue
|
||||
}
|
||||
}
|
||||
// Fallback when cycle metadata and rows disagree after a partial upsert.
|
||||
if schemaNodes[i].PostureCheckSeverity != schema.SeverityUnknown {
|
||||
for _, v := range cycles {
|
||||
modelsNodes[i].PostureChecksViolations = v
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -490,7 +518,6 @@ func GetNodesByIDs(ids []string) (map[string]models.Node, error) {
|
||||
ensureNodeMutex(n)
|
||||
modelsNodes[i] = *n
|
||||
}
|
||||
attachPostureViolations(ctx, _nodes, modelsNodes)
|
||||
|
||||
result := make(map[string]models.Node, len(modelsNodes))
|
||||
for i := range modelsNodes {
|
||||
@@ -659,7 +686,8 @@ type nodeConvertOpts struct {
|
||||
type NodeConvertOption func(*nodeConvertOpts)
|
||||
|
||||
// SkipViolations skips the per-node posture_check_violations query.
|
||||
// Use with attachPostureViolations for batched list loads.
|
||||
// Use on list paths: severity/cycle fields on the node row are enough;
|
||||
// violation details come from the dedicated violations API or single-node get.
|
||||
func SkipViolations() NodeConvertOption {
|
||||
return func(o *nodeConvertOpts) { o.skipViolations = true }
|
||||
}
|
||||
|
||||
@@ -155,8 +155,11 @@ func (n *NodeOrchestrator) CreateNode(ctx context.Context, host *schema.Host, ne
|
||||
|
||||
go func(ctx context.Context) {
|
||||
modelsNode := logic.ConvertSchemaNodeToModelsNode(node)
|
||||
deviceInfo := logic.GetPostureCheckDeviceInfoByNode(ctx, modelsNode)
|
||||
oldViolations := modelsNode.PostureChecksViolations
|
||||
|
||||
modelsNode.PostureChecksViolations, modelsNode.PostureCheckViolationSeverityLevel = logic.CheckPostureViolations(ctx, logic.GetPostureCheckDeviceInfoByNode(ctx, modelsNode), schema.NetworkID(node.Network.Name))
|
||||
modelsNode.PostureChecksViolations, modelsNode.PostureCheckViolationSeverityLevel = logic.CheckPostureViolations(ctx, deviceInfo, schema.NetworkID(node.Network.Name))
|
||||
logic.EmitNewPostureViolationEvents(ctx, oldViolations, modelsNode.PostureChecksViolations, deviceInfo, schema.NetworkID(node.Network.Name))
|
||||
node.PostureCheckSeverity = modelsNode.PostureCheckViolationSeverityLevel
|
||||
node.PostureCheckLastEvaluationCycleID = uuid.NewString()
|
||||
node.PostureCheckLastEvaluatedAt = time.Now().UTC()
|
||||
|
||||
@@ -224,6 +224,7 @@ func InitPro() {
|
||||
logic.ValidateEgressReq = proLogic.ValidateEgressReq
|
||||
logic.CheckPostureViolations = proLogic.CheckPostureViolations
|
||||
logic.CheckPostureViolationsForHost = proLogic.CheckPostureViolationsForHost
|
||||
logic.EmitNewPostureViolationEvents = proLogic.EmitNewPostureViolationEvents
|
||||
logic.GetPostureCheckDeviceInfoByNode = proLogic.GetPostureCheckDeviceInfoByNode
|
||||
logic.SyncHostMDMState = mdmpkg.SyncHostMDMState
|
||||
logic.SyncHostEDRState = edrpkg.SyncHostEDRState
|
||||
|
||||
+9
-1
@@ -107,7 +107,15 @@ func EventWatcher() {
|
||||
Diff: diff,
|
||||
TimeStamp: time.Now().UTC(),
|
||||
}
|
||||
a.Create(db.WithContext(context.TODO()))
|
||||
writeCtx := db.WithContext(context.TODO())
|
||||
if e.TenantID != "" {
|
||||
writeCtx = scope.WithContext(writeCtx, scope.TenantScope, e.TenantID)
|
||||
}
|
||||
if err := a.Create(writeCtx); err != nil {
|
||||
slog.Error("failed to persist audit event",
|
||||
"action", e.Action, "network", e.NetworkID, "tenant", e.TenantID, "error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
_siemMtx.Lock()
|
||||
if !_pushToSiem {
|
||||
|
||||
+74
-84
@@ -92,10 +92,21 @@ func RunPostureChecksForTenant(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nodes, err := logic.GetAllNodes(ctx)
|
||||
nodes, err := logic.GetAllNodesWithViolations(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// One-time backfill: if this tenant has never logged a posture failure,
|
||||
// treat previous violations as empty so current failures appear in activity.
|
||||
// After the first POSTURE_CHECK_FAILED is written, resume new-only dedupe.
|
||||
backfillPostureFailures := false
|
||||
if has, err := (&schema.Event{}).HasAction(ctx, schema.PostureCheckFailed); err != nil {
|
||||
slog.Warn("failed to check for existing posture failure events", "error", err)
|
||||
} else {
|
||||
backfillPostureFailures = !has
|
||||
}
|
||||
|
||||
for _, netI := range nets {
|
||||
networkNodes := logic.GetNetworkNodesMemory(nodes, netI.Name)
|
||||
if len(networkNodes) == 0 {
|
||||
@@ -126,22 +137,35 @@ func RunPostureChecksForTenant(ctx context.Context) error {
|
||||
if noChecks && len(extclient.PostureChecksViolations) == 0 {
|
||||
continue
|
||||
}
|
||||
emitNewMDMViolationEvents(ctx, extclient.PostureChecksViolations, postureChecksViolations, deviceInfo, schema.NetworkID(netI.Name))
|
||||
emitNewEDRViolationEvents(ctx, extclient.PostureChecksViolations, postureChecksViolations, deviceInfo, schema.NetworkID(netI.Name))
|
||||
oldVi := extclient.PostureChecksViolations
|
||||
if backfillPostureFailures {
|
||||
oldVi = nil
|
||||
}
|
||||
EmitNewPostureViolationEvents(ctx, oldVi, postureChecksViolations, deviceInfo, schema.NetworkID(netI.Name))
|
||||
extclient.PostureChecksViolations = postureChecksViolations
|
||||
extclient.PostureCheckVolationSeverityLevel = postureCheckVolationSeverityLevel
|
||||
extclient.LastEvaluatedAt = time.Now().UTC()
|
||||
logic.SaveExtClient(ctx, &extclient)
|
||||
}
|
||||
} else {
|
||||
if noChecks && len(nodeI.PostureChecksViolations) == 0 {
|
||||
// When no checks are configured and the node is already clean, skip
|
||||
// a full cycle rewrite — but still purge any orphaned historical
|
||||
// violation rows left by older upserts that only appended.
|
||||
if noChecks &&
|
||||
nodeI.PostureCheckViolationSeverityLevel == schema.SeverityUnknown &&
|
||||
len(nodeI.PostureChecksViolations) == 0 {
|
||||
_ = (&schema.Node{ID: nodeI.ID.String(), TenantID: nodeI.TenantID}).DeleteViolations(ctx)
|
||||
continue
|
||||
}
|
||||
emitNewMDMViolationEvents(ctx, nodeI.PostureChecksViolations, postureChecksViolations, deviceInfo, schema.NetworkID(netI.Name))
|
||||
emitNewEDRViolationEvents(ctx, nodeI.PostureChecksViolations, postureChecksViolations, deviceInfo, schema.NetworkID(netI.Name))
|
||||
oldVi := nodeI.PostureChecksViolations
|
||||
if backfillPostureFailures {
|
||||
oldVi = nil
|
||||
}
|
||||
EmitNewPostureViolationEvents(ctx, oldVi, postureChecksViolations, deviceInfo, schema.NetworkID(netI.Name))
|
||||
|
||||
_node := &schema.Node{
|
||||
ID: nodeI.ID.String(),
|
||||
TenantID: nodeI.TenantID,
|
||||
PostureCheckSeverity: postureCheckVolationSeverityLevel,
|
||||
PostureCheckLastEvaluationCycleID: uuid.NewString(),
|
||||
PostureCheckLastEvaluatedAt: time.Now().UTC(),
|
||||
@@ -161,7 +185,10 @@ func RunPostureChecksForTenant(ctx context.Context) error {
|
||||
EvaluatedAt: _node.PostureCheckLastEvaluatedAt,
|
||||
})
|
||||
}
|
||||
_ = _node.UpsertViolations(ctx, _violations)
|
||||
if err := _node.UpsertViolations(ctx, _violations); err != nil {
|
||||
slog.Error("failed to upsert posture check violations",
|
||||
"node_id", _node.ID, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -942,94 +969,54 @@ func asInt(v interface{}) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
// emitNewMDMViolationEvents emits a posture_check_failed audit event for every
|
||||
// MDM compliance violation that is newly present (not in oldVi) in newVi.
|
||||
// Old violations don't re-fire; cleared violations are also ignored here.
|
||||
func emitNewMDMViolationEvents(ctx context.Context, oldVi, newVi []models.Violation, d models.PostureCheckDeviceInfo, network schema.NetworkID) {
|
||||
// EmitNewPostureViolationEvents emits a posture_check_failed audit event for
|
||||
// every violation newly present in newVi (any attribute). Dedupes by CheckID
|
||||
// against oldVi so ongoing failures do not re-fire; cleared violations are
|
||||
// ignored. MDM/EDR events keep their provider-specific enrichment fields.
|
||||
func EmitNewPostureViolationEvents(ctx context.Context, oldVi, newVi []models.Violation, d models.PostureCheckDeviceInfo, network schema.NetworkID) {
|
||||
if len(newVi) == 0 {
|
||||
return
|
||||
}
|
||||
prev := make(map[string]struct{}, len(oldVi))
|
||||
for _, v := range oldVi {
|
||||
prev[v.CheckID+"|"+v.Message] = struct{}{}
|
||||
}
|
||||
providerID, _ := mdmpkg.ActiveProviderID(ctx)
|
||||
for _, v := range newVi {
|
||||
if v.Attribute != string(schema.MDMCompliance) {
|
||||
if v.CheckID == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := prev[v.CheckID+"|"+v.Message]; ok {
|
||||
continue
|
||||
}
|
||||
diff := models.Diff{
|
||||
Old: nil,
|
||||
New: map[string]interface{}{
|
||||
"event": "posture_check_failed",
|
||||
"type": string(schema.MDMCompliance),
|
||||
"host_id": d.HostID,
|
||||
"check_id": v.CheckID,
|
||||
"check": v.Name,
|
||||
"reason": v.Message,
|
||||
"severity": v.Severity,
|
||||
"provider": providerID,
|
||||
"enrolled": mdmStateEnrolled(d.MDMState),
|
||||
"compliant": mdmStateCompliant(d.MDMState),
|
||||
},
|
||||
}
|
||||
logic.LogEvent(ctx, &models.Event{
|
||||
Action: schema.PostureCheckFailed,
|
||||
Source: models.Subject{
|
||||
ID: d.HostID,
|
||||
Name: d.HostID,
|
||||
Type: schema.DeviceSub,
|
||||
},
|
||||
TriggeredBy: "system",
|
||||
Target: models.Subject{
|
||||
ID: v.CheckID,
|
||||
Name: v.Name,
|
||||
Type: schema.PostureCheckSub,
|
||||
},
|
||||
NetworkID: network,
|
||||
Origin: schema.Api,
|
||||
Diff: diff,
|
||||
})
|
||||
prev[v.CheckID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// emitNewEDRViolationEvents emits a posture_check_failed audit event for every
|
||||
// EDR compliance violation that is newly present (not in oldVi) in newVi.
|
||||
// Old violations don't re-fire; cleared violations are also ignored here.
|
||||
func emitNewEDRViolationEvents(ctx context.Context, oldVi, newVi []models.Violation, d models.PostureCheckDeviceInfo, network schema.NetworkID) {
|
||||
if len(newVi) == 0 {
|
||||
return
|
||||
}
|
||||
prev := make(map[string]struct{}, len(oldVi))
|
||||
for _, v := range oldVi {
|
||||
prev[v.CheckID+"|"+v.Message] = struct{}{}
|
||||
}
|
||||
providerID, _ := edrpkg.ActiveProviderID(ctx)
|
||||
var mdmProviderID, edrProviderID string
|
||||
for _, v := range newVi {
|
||||
if v.Attribute != string(schema.EDRCompliance) {
|
||||
continue
|
||||
if v.CheckID != "" {
|
||||
if _, ok := prev[v.CheckID]; ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if _, ok := prev[v.CheckID+"|"+v.Message]; ok {
|
||||
continue
|
||||
payload := map[string]interface{}{
|
||||
"event": "posture_check_failed",
|
||||
"type": v.Attribute,
|
||||
"host_id": d.HostID,
|
||||
"check_id": v.CheckID,
|
||||
"check": v.Name,
|
||||
"reason": v.Message,
|
||||
"severity": v.Severity,
|
||||
}
|
||||
diff := models.Diff{
|
||||
Old: nil,
|
||||
New: map[string]interface{}{
|
||||
"event": "posture_check_failed",
|
||||
"type": string(schema.EDRCompliance),
|
||||
"host_id": d.HostID,
|
||||
"check_id": v.CheckID,
|
||||
"check": v.Name,
|
||||
"reason": v.Message,
|
||||
"severity": v.Severity,
|
||||
"provider": providerID,
|
||||
"agent_installed": edrStateAgentInstalled(d.EDRState),
|
||||
"agent_healthy": edrStateAgentHealthy(d.EDRState),
|
||||
"risk_level": edrStateRiskLevel(d.EDRState),
|
||||
},
|
||||
switch v.Attribute {
|
||||
case string(schema.MDMCompliance):
|
||||
if mdmProviderID == "" {
|
||||
mdmProviderID, _ = mdmpkg.ActiveProviderID(ctx)
|
||||
}
|
||||
payload["provider"] = mdmProviderID
|
||||
payload["enrolled"] = mdmStateEnrolled(d.MDMState)
|
||||
payload["compliant"] = mdmStateCompliant(d.MDMState)
|
||||
case string(schema.EDRCompliance):
|
||||
if edrProviderID == "" {
|
||||
edrProviderID, _ = edrpkg.ActiveProviderID(ctx)
|
||||
}
|
||||
payload["provider"] = edrProviderID
|
||||
payload["agent_installed"] = edrStateAgentInstalled(d.EDRState)
|
||||
payload["agent_healthy"] = edrStateAgentHealthy(d.EDRState)
|
||||
payload["risk_level"] = edrStateRiskLevel(d.EDRState)
|
||||
}
|
||||
logic.LogEvent(ctx, &models.Event{
|
||||
Action: schema.PostureCheckFailed,
|
||||
@@ -1046,7 +1033,10 @@ func emitNewEDRViolationEvents(ctx context.Context, oldVi, newVi []models.Violat
|
||||
},
|
||||
NetworkID: network,
|
||||
Origin: schema.Api,
|
||||
Diff: diff,
|
||||
Diff: models.Diff{
|
||||
Old: nil,
|
||||
New: payload,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,18 @@ func (a *Event) List(ctx context.Context, from, to time.Time) (ats []Event, err
|
||||
return
|
||||
}
|
||||
|
||||
// HasAction returns true if at least one event with the given action exists
|
||||
// for the current tenant (used for one-time posture-failure audit backfill).
|
||||
func (a *Event) HasAction(ctx context.Context, action Action) (bool, error) {
|
||||
query := db.FromContext(ctx).Model(&Event{}).Where("action = ?", action)
|
||||
if tenantID := scope.ID(ctx); tenantID != "" {
|
||||
query = dbtypes.WithFilter(fmt.Sprintf("%s.tenant_id", eventsTable), tenantID)(query)
|
||||
}
|
||||
var count int64
|
||||
err := query.Limit(1).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
|
||||
func (a *Event) DeleteAllForTenant(ctx context.Context) error {
|
||||
if tenantID := scope.ID(ctx); tenantID != "" {
|
||||
return db.FromContext(ctx).Where(fmt.Sprintf("%s.tenant_id = ?", eventsTable), tenantID).Delete(&Event{}).Error
|
||||
|
||||
+69
-12
@@ -185,41 +185,98 @@ func (n *Node) Count(ctx context.Context, options ...dbtypes.Option) (int, error
|
||||
return int(count), err
|
||||
}
|
||||
|
||||
// UpsertViolations replaces stored violations for this node with the latest
|
||||
// evaluation cycle, then updates severity / cycle metadata on the node row.
|
||||
// Inserts the new cycle and updates the node first, then deletes older cycles,
|
||||
// so readers never observe a window with severity set and zero violation rows.
|
||||
func (n *Node) UpsertViolations(ctx context.Context, violations []PostureCheckViolation) error {
|
||||
txCtx := db.BeginTx(ctx)
|
||||
tx := db.FromContext(txCtx)
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
cycleID := n.PostureCheckLastEvaluationCycleID
|
||||
|
||||
if len(violations) > 0 {
|
||||
for i := range violations {
|
||||
if violations[i].TenantID == "" {
|
||||
violations[i].TenantID = n.TenantID
|
||||
}
|
||||
if violations[i].NodeID == "" {
|
||||
violations[i].NodeID = n.ID
|
||||
}
|
||||
if violations[i].EvaluationCycleID == "" {
|
||||
violations[i].EvaluationCycleID = cycleID
|
||||
}
|
||||
}
|
||||
|
||||
err := db.FromContext(ctx).Model(&PostureCheckViolation{}).Create(&violations).Error
|
||||
if err != nil {
|
||||
if err := tx.Model(&PostureCheckViolation{}).Create(&violations).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return db.FromContext(ctx).Model(&Node{}).
|
||||
if err := tx.Model(&Node{}).
|
||||
Where("id = ?", n.ID).
|
||||
Update("posture_check_severity", n.PostureCheckSeverity).
|
||||
Update("posture_check_last_evaluation_cycle_id", n.PostureCheckLastEvaluationCycleID).
|
||||
Update("posture_check_last_evaluated_at", n.PostureCheckLastEvaluatedAt).
|
||||
Error
|
||||
Updates(map[string]interface{}{
|
||||
"posture_check_severity": n.PostureCheckSeverity,
|
||||
"posture_check_last_evaluation_cycle_id": cycleID,
|
||||
"posture_check_last_evaluated_at": n.PostureCheckLastEvaluatedAt,
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
del := tx.Model(&PostureCheckViolation{}).Where("node_id = ?", n.ID)
|
||||
if tenantID := scope.ID(ctx); tenantID != "" {
|
||||
del = dbtypes.WithFilter(fmt.Sprintf("%s.tenant_id", postureCheckViolationsTable), tenantID)(del)
|
||||
}
|
||||
if cycleID != "" {
|
||||
// Keep the cycle we just wrote; drop historical rows.
|
||||
del = del.Where("evaluation_cycle_id <> ?", cycleID)
|
||||
}
|
||||
if err := del.Delete(&PostureCheckViolation{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
}
|
||||
committed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *Node) ListViolations(ctx context.Context) ([]PostureCheckViolation, error) {
|
||||
var violations []PostureCheckViolation
|
||||
query := db.FromContext(ctx).Model(&PostureCheckViolation{}).
|
||||
Where("node_id = ? AND evaluation_cycle_id = ?", n.ID, n.PostureCheckLastEvaluationCycleID)
|
||||
query := db.FromContext(ctx).Model(&PostureCheckViolation{}).Where("node_id = ?", n.ID)
|
||||
if tenantID := scope.ID(ctx); tenantID != "" {
|
||||
query = dbtypes.WithFilter(fmt.Sprintf("%s.tenant_id", postureCheckViolationsTable), tenantID)(query)
|
||||
}
|
||||
err := query.Find(&violations).Error
|
||||
|
||||
if n.PostureCheckLastEvaluationCycleID != "" {
|
||||
err := query.Where("evaluation_cycle_id = ?", n.PostureCheckLastEvaluationCycleID).Find(&violations).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(violations) > 0 || n.PostureCheckSeverity == SeverityUnknown {
|
||||
return violations, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback when severity says violated but the stored cycle has no rows
|
||||
// (partial upsert from older delete-first writers).
|
||||
fallback := db.FromContext(ctx).Model(&PostureCheckViolation{}).Where("node_id = ?", n.ID)
|
||||
if tenantID := scope.ID(ctx); tenantID != "" {
|
||||
fallback = dbtypes.WithFilter(fmt.Sprintf("%s.tenant_id", postureCheckViolationsTable), tenantID)(fallback)
|
||||
}
|
||||
err := fallback.Find(&violations).Error
|
||||
return violations, err
|
||||
}
|
||||
|
||||
// ListViolationsByNodeIDs fetches posture violations for many nodes in one query.
|
||||
// Callers should filter by each node's PostureCheckLastEvaluationCycleID in memory.
|
||||
// Prefer after UpsertViolations (which replaces cycles) so only current rows remain.
|
||||
// Callers should still filter by each node's PostureCheckLastEvaluationCycleID.
|
||||
func ListViolationsByNodeIDs(ctx context.Context, nodeIDs []string) ([]PostureCheckViolation, error) {
|
||||
if len(nodeIDs) == 0 {
|
||||
return nil, nil
|
||||
|
||||
Reference in New Issue
Block a user