Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions compliance/compliance.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,15 @@ func (c *Compliance) runRecv(ctx context.Context, client sensor.ComplianceServic
default:
log.Errorf("Unknown ACK Action: %s", t.Ack.GetAction())
}
case *sensor.MsgToCompliance_ComplianceAck:
complianceAck := t.ComplianceAck
log.Debugf("Received ComplianceACK: type=%s, action=%s, resource_id=%s, reason=%s",
complianceAck.GetMessageType(),
complianceAck.GetAction(),
complianceAck.GetResourceId(),
complianceAck.GetReason(),
)
// TODO: Handle ComplianceACK message from Sensor/Central 4.10.
default:
utils.Should(errors.Errorf("Unhandled msg type: %T", t))
}
Expand Down
115 changes: 110 additions & 5 deletions sensor/common/compliance/node_inventory_handler_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func (c *nodeInventoryHandlerImpl) Stopped() concurrency.ReadOnlyErrorSignal {
}

func (c *nodeInventoryHandlerImpl) Capabilities() []centralsensor.SensorCapability {
return nil
return []centralsensor.SensorCapability{centralsensor.SensorACKSupport}
}

// ResponsesC returns a channel with messages to Central. It must be called after Start() for the channel to be not nil
Expand Down Expand Up @@ -115,15 +115,79 @@ func (c *nodeInventoryHandlerImpl) Notify(e common.SensorComponentEvent) {
}

func (c *nodeInventoryHandlerImpl) Accepts(msg *central.MsgToSensor) bool {
return msg.GetNodeInventoryAck() != nil
if msg.GetNodeInventoryAck() != nil {
return true
}
if sensorAck := msg.GetSensorAck(); sensorAck != nil {
switch sensorAck.GetMessageType() {
case central.SensorACK_NODE_INVENTORY, central.SensorACK_NODE_INDEX_REPORT:
return true
}
}
return false
}

func (c *nodeInventoryHandlerImpl) ProcessMessage(_ context.Context, msg *central.MsgToSensor) error {
ackMsg := msg.GetNodeInventoryAck()
if ackMsg == nil {
// Handle new SensorACK message (from Central 4.10+)
if sensorAck := msg.GetSensorAck(); sensorAck != nil {
return c.processSensorACK(sensorAck)
}

// Handle legacy NodeInventoryACK message (from Central 4.9 and earlier)
if ackMsg := msg.GetNodeInventoryAck(); ackMsg != nil {
return c.processNodeInventoryACK(ackMsg)
}

return nil
}

// processSensorACK handles the new generic SensorACK message from Central.
// Only node-related ACK/NACK messages (NODE_INVENTORY, NODE_INDEX_REPORT) are forwarded to Compliance.
// All other message types are ignored - they should be handled by their respective handlers.
func (c *nodeInventoryHandlerImpl) processSensorACK(sensorAck *central.SensorACK) error {
log.Debugf("Received SensorACK message: type=%s, action=%s, resource_id=%s, reason=%s",
sensorAck.GetMessageType(), sensorAck.GetAction(), sensorAck.GetResourceId(), sensorAck.GetReason())

metrics.ObserveNodeScanningAck(sensorAck.GetResourceId(),
sensorAck.GetAction().String(),
sensorAck.GetMessageType().String(),
metrics.AckOperationReceive,
"", metrics.AckOriginSensor)

// Only handle node-related message types - all others are handled by their respective handlers
var messageType sensor.MsgToCompliance_ComplianceACK_MessageType
switch sensorAck.GetMessageType() {
case central.SensorACK_NODE_INVENTORY:
messageType = sensor.MsgToCompliance_ComplianceACK_NODE_INVENTORY
case central.SensorACK_NODE_INDEX_REPORT:
messageType = sensor.MsgToCompliance_ComplianceACK_NODE_INDEX_REPORT
default:
// Not a node-related message - ignore it (handled by other handlers like VM handler)
log.Debugf("Ignoring SensorACK message type %s - not handled by node inventory handler", sensorAck.GetMessageType())
return nil
}

// Map central.SensorACK action to sensor.ComplianceACK action
var action sensor.MsgToCompliance_ComplianceACK_Action
switch sensorAck.GetAction() {
case central.SensorACK_ACK:
action = sensor.MsgToCompliance_ComplianceACK_ACK
case central.SensorACK_NACK:
action = sensor.MsgToCompliance_ComplianceACK_NACK
default:
log.Debugf("Ignoring SensorACK message with unknown action %s: type=%s, resource_id=%s, reason=%s",
sensorAck.GetAction(), sensorAck.GetMessageType(), sensorAck.GetResourceId(), sensorAck.GetReason())
return nil
}
log.Debugf("Received node-scanning-ACK message of type %s, action %s for node %s",

c.sendComplianceAckToCompliance(sensorAck.GetResourceId(), action, messageType, sensorAck.GetReason())
return nil
}

// processNodeInventoryACK handles the legacy NodeInventoryACK message from Central 4.9 and earlier.
// It forwards the ACK/NACK to Compliance using the legacy NodeInventoryACK message type.
func (c *nodeInventoryHandlerImpl) processNodeInventoryACK(ackMsg *central.NodeInventoryACK) error {
log.Debugf("Received legacy node-scanning-ACK message of type %s, action %s for node %s",
ackMsg.GetMessageType(), ackMsg.GetAction(), ackMsg.GetNodeName())
metrics.ObserveNodeScanningAck(ackMsg.GetNodeName(),
ackMsg.GetAction().String(),
Expand Down Expand Up @@ -284,6 +348,47 @@ func (c *nodeInventoryHandlerImpl) sendAckToCompliance(
reason, metrics.AckOriginSensor)
}

// sendComplianceAckToCompliance sends the new ComplianceACK message to Compliance.
// This is used for the new SensorACK message from Central 4.10+.
func (c *nodeInventoryHandlerImpl) sendComplianceAckToCompliance(
resourceID string,
action sensor.MsgToCompliance_ComplianceACK_Action,
messageType sensor.MsgToCompliance_ComplianceACK_MessageType,
reason string,
) {
select {
case <-c.stopper.Flow().StopRequested():
log.Debugf("Skipped sending ComplianceACK (stop requested): type=%s, action=%s, resource_id=%s, reason=%s",
messageType, action, resourceID, reason)
case c.toCompliance <- common.MessageToComplianceWithAddress{
Msg: &sensor.MsgToCompliance{
Msg: &sensor.MsgToCompliance_ComplianceAck{
ComplianceAck: &sensor.MsgToCompliance_ComplianceACK{
Action: action,
MessageType: messageType,
ResourceId: resourceID,
Reason: reason,
},
},
},
Hostname: resourceID, // For node-based messages, resourceID is the node name
Broadcast: resourceID == "",
}:
log.Debugf("Sent ComplianceACK to Compliance: type=%s, action=%s, resource_id=%s, reason=%s",
messageType, action, resourceID, reason)

// Record old metric for compatiblity.
// Note the new 'reason' is set in Central and is a string, not an enum, thus hardcoding here to 'forward'.
// The new metric for the SensorACK records the reason fully (as a string).
metrics.ObserveNodeScanningAck(resourceID,
action.String(),
messageType.String(),
metrics.AckOperationSend,
metrics.AckReasonForwardingFromCentral,
metrics.AckOriginSensor)
}
}

func (c *nodeInventoryHandlerImpl) sendNodeInventory(toC chan<- *message.ExpiringMessage, inventory *storage.NodeInventory) {
if inventory == nil {
return
Expand Down
210 changes: 209 additions & 1 deletion sensor/common/compliance/node_inventory_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
v4 "github.com/stackrox/rox/generated/internalapi/scanner/v4"
"github.com/stackrox/rox/generated/internalapi/sensor"
"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/centralsensor"
"github.com/stackrox/rox/pkg/concurrency"
"github.com/stackrox/rox/pkg/protocompat"
"github.com/stackrox/rox/pkg/testutils/goleak"
Expand Down Expand Up @@ -219,7 +220,9 @@ func (s *NodeInventoryHandlerTestSuite) TestCapabilities() {
reports := make(chan *index.IndexReportWrap)
defer close(reports)
h := NewNodeInventoryHandler(inventories, reports, &mockAlwaysHitNodeIDMatcher{}, &mockRHCOSNodeMatcher{})
s.Nil(h.Capabilities())
caps := h.Capabilities()
s.Require().Len(caps, 1)
s.Equal(centralsensor.SensorACKSupport, caps[0])
}

func (s *NodeInventoryHandlerTestSuite) TestResponsesCShouldPanicWhenNotStarted() {
Expand Down Expand Up @@ -372,6 +375,211 @@ func (s *NodeInventoryHandlerTestSuite) TestHandlerCentralACKsToCompliance() {

}

// TestHandlerSensorACKsToCompliance tests the new SensorACK message handling.
// Node-related SensorACK messages from Central 4.10+ should be forwarded to Compliance as ComplianceACK.
// Non-node messages (like VM_INDEX_REPORT) should be ignored.
func (s *NodeInventoryHandlerTestSuite) TestHandlerSensorACKsToCompliance() {
cases := map[string]struct {
sensorACK *central.SensorACK
shouldForward bool // true if message should be forwarded to Compliance
expectedAction sensor.MsgToCompliance_ComplianceACK_Action
expectedMessageType sensor.MsgToCompliance_ComplianceACK_MessageType
expectedReason string
expectedHostname string
expectedBroadcast bool
}{
"NODE_INVENTORY ACK should be forwarded": {
sensorACK: &central.SensorACK{
Action: central.SensorACK_ACK,
MessageType: central.SensorACK_NODE_INVENTORY,
ResourceId: "node-1",
},
shouldForward: true,
expectedAction: sensor.MsgToCompliance_ComplianceACK_ACK,
expectedMessageType: sensor.MsgToCompliance_ComplianceACK_NODE_INVENTORY,
},
"NODE_INVENTORY NACK should be forwarded with reason": {
sensorACK: &central.SensorACK{
Action: central.SensorACK_NACK,
MessageType: central.SensorACK_NODE_INVENTORY,
ResourceId: "node-1",
Reason: "some failure reason",
},
shouldForward: true,
expectedAction: sensor.MsgToCompliance_ComplianceACK_NACK,
expectedMessageType: sensor.MsgToCompliance_ComplianceACK_NODE_INVENTORY,
expectedReason: "some failure reason",
},
"NODE_INDEX_REPORT ACK should be forwarded": {
sensorACK: &central.SensorACK{
Action: central.SensorACK_ACK,
MessageType: central.SensorACK_NODE_INDEX_REPORT,
ResourceId: "node-2",
},
shouldForward: true,
expectedAction: sensor.MsgToCompliance_ComplianceACK_ACK,
expectedMessageType: sensor.MsgToCompliance_ComplianceACK_NODE_INDEX_REPORT,
},
"NODE_INDEX_REPORT NACK should be forwarded with reason": {
sensorACK: &central.SensorACK{
Action: central.SensorACK_NACK,
MessageType: central.SensorACK_NODE_INDEX_REPORT,
ResourceId: "node-2",
Reason: "index failure",
},
shouldForward: true,
expectedAction: sensor.MsgToCompliance_ComplianceACK_NACK,
expectedMessageType: sensor.MsgToCompliance_ComplianceACK_NODE_INDEX_REPORT,
expectedReason: "index failure",
},
"NODE_INDEX_REPORT with unknown action should be ignored": {
sensorACK: &central.SensorACK{
Action: central.SensorACK_Action(999),
MessageType: central.SensorACK_NODE_INDEX_REPORT,
ResourceId: "node-2",
},
shouldForward: false,
},
"Broadcast NODE_INVENTORY ACK should set broadcast": {
sensorACK: &central.SensorACK{
Action: central.SensorACK_ACK,
MessageType: central.SensorACK_NODE_INVENTORY,
ResourceId: "",
},
shouldForward: true,
expectedAction: sensor.MsgToCompliance_ComplianceACK_ACK,
expectedMessageType: sensor.MsgToCompliance_ComplianceACK_NODE_INVENTORY,
expectedHostname: "",
expectedBroadcast: true,
},
"VM_INDEX_REPORT ACK should be ignored": {
sensorACK: &central.SensorACK{
Action: central.SensorACK_ACK,
MessageType: central.SensorACK_VM_INDEX_REPORT,
ResourceId: "vm-1",
},
shouldForward: false,
},
}

for name, tc := range cases {
s.Run(name, func() {
ch := make(chan *storage.NodeInventory)
defer close(ch)
reports := make(chan *index.IndexReportWrap)
defer close(reports)
handler := NewNodeInventoryHandler(ch, reports, &mockAlwaysHitNodeIDMatcher{}, &mockRHCOSNodeMatcher{})
s.NoError(handler.Start())
handler.Notify(common.SensorComponentEventCentralReachable)

if tc.shouldForward {
// Start a goroutine to receive the ComplianceACK before sending
// (channel is unbuffered so we need a receiver ready)
msgCh := make(chan common.MessageToComplianceWithAddress, 1)
errCh := make(chan error, 1)
go func() {
select {
case msg := <-handler.ComplianceC():
msgCh <- msg
case <-time.After(3 * time.Second):
errCh <- errors.New("ComplianceACK message not received within 3 seconds")
}
}()

// Send the SensorACK message
err := handler.ProcessMessage(s.T().Context(), &central.MsgToSensor{
Msg: &central.MsgToSensor_SensorAck{SensorAck: tc.sensorACK},
})
s.NoError(err)

select {
case err := <-errCh:
s.Fail(err.Error())
case msg := <-msgCh:
// Verify ComplianceACK was sent to Compliance
complianceAck := msg.Msg.GetComplianceAck()
s.Require().NotNil(complianceAck, "Expected ComplianceACK message")
s.Equal(tc.expectedAction, complianceAck.GetAction())
s.Equal(tc.expectedMessageType, complianceAck.GetMessageType())
s.Equal(tc.sensorACK.GetResourceId(), complianceAck.GetResourceId())
s.Equal(tc.expectedReason, complianceAck.GetReason())
if tc.expectedHostname != "" || tc.expectedBroadcast {
s.Equal(tc.expectedHostname, msg.Hostname)
s.Equal(tc.expectedBroadcast, msg.Broadcast)
} else {
s.Equal(tc.sensorACK.GetResourceId(), msg.Hostname)
s.False(msg.Broadcast)
}
}

} else {
// Send the SensorACK message
err := handler.ProcessMessage(s.T().Context(), &central.MsgToSensor{
Msg: &central.MsgToSensor_SensorAck{SensorAck: tc.sensorACK},
})
s.NoError(err)

// Verify no message arrives within the timeout.
select {
case msg := <-handler.ComplianceC():
s.Failf("Message should not be forwarded to Compliance", "got: %v", msg)
case <-time.After(20 * time.Millisecond):
// Expected: nothing received.
}
}

handler.Stop()
s.NoError(handler.Stopped().Wait())
})
}
}

// TestHandlerAcceptsBothAckTypes tests that the handler accepts both legacy NodeInventoryACK
// and new SensorACK message types.
func (s *NodeInventoryHandlerTestSuite) TestHandlerAcceptsBothAckTypes() {
ch := make(chan *storage.NodeInventory)
defer close(ch)
reports := make(chan *index.IndexReportWrap)
defer close(reports)
handler := NewNodeInventoryHandler(ch, reports, &mockAlwaysHitNodeIDMatcher{}, &mockRHCOSNodeMatcher{})

// Test legacy NodeInventoryACK
legacyMsg := &central.MsgToSensor{
Msg: &central.MsgToSensor_NodeInventoryAck{NodeInventoryAck: &central.NodeInventoryACK{
ClusterId: "cluster-1",
NodeName: "node-1",
Action: central.NodeInventoryACK_ACK,
}},
}
s.True(handler.Accepts(legacyMsg), "Handler should accept legacy NodeInventoryACK")

// Test new SensorACK for node-related messages
nodeAckMsg := &central.MsgToSensor{
Msg: &central.MsgToSensor_SensorAck{SensorAck: &central.SensorACK{
Action: central.SensorACK_ACK,
MessageType: central.SensorACK_NODE_INDEX_REPORT,
ResourceId: "node-1",
}},
}
s.True(handler.Accepts(nodeAckMsg), "Handler should accept SensorACK for node messages")

// Test SensorACK for VM messages (should be handled by VM handler, not accepted here)
vmAckMsg := &central.MsgToSensor{
Msg: &central.MsgToSensor_SensorAck{SensorAck: &central.SensorACK{
Action: central.SensorACK_ACK,
MessageType: central.SensorACK_VM_INDEX_REPORT,
ResourceId: "vm-1",
}},
}
s.False(handler.Accepts(vmAckMsg), "Handler should not accept SensorACK for VM messages")

// Test message without ACK
otherMsg := &central.MsgToSensor{
Msg: &central.MsgToSensor_ClusterConfig{},
}
s.False(handler.Accepts(otherMsg), "Handler should not accept other message types")
}

// This test simulates a running Sensor loosing connection to Central, followed by a reconnect.
// As soon as Sensor enters offline mode, it should send NACKs to Compliance.
// In online mode, inventories are forwarded to Central, which responds with an ACK, that is passed to Compliance.
Expand Down
Loading
Loading