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
57 changes: 38 additions & 19 deletions sensor/common/networkflow/manager/enrichment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/stackrox/rox/pkg/net"
"github.com/stackrox/rox/pkg/networkgraph"
"github.com/stackrox/rox/pkg/timestamp"
"github.com/stackrox/rox/sensor/common/networkflow/manager/indicator"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
Expand Down Expand Up @@ -95,7 +96,7 @@ func TestEnrichConnection_BusinessLogicPaths(t *testing.T) {
setupFeatureFlags func(*testing.T)
expectedResult EnrichmentResult
expectedReason EnrichmentReasonConn
validateEnrichment func(*testing.T, map[networkConnIndicator]timestamp.MicroTS)
validateEnrichment func(*testing.T, map[indicator.NetworkConn]timestamp.MicroTS)
}{
"IP parsing error caused by malformed address should yield result EnrichmentResultInvalidInput with reason EnrichmentReasonConnParsingIPFailed": {
setupConnection: func() (*connection, *connStatus) {
Expand Down Expand Up @@ -126,10 +127,10 @@ func TestEnrichConnection_BusinessLogicPaths(t *testing.T) {
},
expectedResult: EnrichmentResultSuccess,
expectedReason: EnrichmentReasonConnSuccess,
validateEnrichment: func(t *testing.T, enriched map[networkConnIndicator]timestamp.MicroTS) {
validateEnrichment: func(t *testing.T, enriched map[indicator.NetworkConn]timestamp.MicroTS) {
assert.Len(t, enriched, 1, "Should have one enriched connection")
for indicator := range enriched {
assert.Equal(t, "external-network-id", indicator.dstEntity.ID, "Should use external source entity")
assert.Equal(t, "external-network-id", indicator.DstEntity.ID, "Should use external source entity")
}
},
},
Expand Down Expand Up @@ -165,10 +166,10 @@ func TestEnrichConnection_BusinessLogicPaths(t *testing.T) {
},
expectedResult: EnrichmentResultSuccess,
expectedReason: EnrichmentReasonConnSuccess,
validateEnrichment: func(t *testing.T, enriched map[networkConnIndicator]timestamp.MicroTS) {
validateEnrichment: func(t *testing.T, enriched map[indicator.NetworkConn]timestamp.MicroTS) {
assert.Len(t, enriched, 1, "Should have one enriched connection")
for indicator := range enriched {
assert.Equal(t, networkgraph.InternetEntity().ID, indicator.dstEntity.ID, "Should use Internet entity")
assert.Equal(t, networkgraph.InternetEntity().ID, indicator.DstEntity.ID, "Should use Internet entity")
}
},
},
Expand All @@ -186,13 +187,13 @@ func TestEnrichConnection_BusinessLogicPaths(t *testing.T) {
},
expectedResult: EnrichmentResultSuccess,
expectedReason: EnrichmentReasonConnSuccess,
validateEnrichment: func(t *testing.T, enriched map[networkConnIndicator]timestamp.MicroTS) {
validateEnrichment: func(t *testing.T, enriched map[indicator.NetworkConn]timestamp.MicroTS) {
assert.Len(t, enriched, 1, "Should have one enriched connection")
// For internal connections without external source, it creates a fallback entity
// This tests the fallback behavior for unknown internal addresses
for indicator := range enriched {
// Should enrich successfully regardless of entity type
assert.NotEmpty(t, indicator.dstEntity.ID, "Should have a valid destination entity")
assert.NotEmpty(t, indicator.DstEntity.ID, "Should have a valid destination entity")
}
},
},
Expand All @@ -218,7 +219,7 @@ func TestEnrichConnection_BusinessLogicPaths(t *testing.T) {
},
expectedResult: EnrichmentResultSuccess,
expectedReason: EnrichmentReasonConnSuccess,
validateEnrichment: func(t *testing.T, enriched map[networkConnIndicator]timestamp.MicroTS) {
validateEnrichment: func(t *testing.T, enriched map[indicator.NetworkConn]timestamp.MicroTS) {
// Should still enrich even with feature disabled
assert.Len(t, enriched, 1, "Should have one enriched connection")
},
Expand Down Expand Up @@ -247,7 +248,7 @@ func TestEnrichConnection_BusinessLogicPaths(t *testing.T) {

// Setup test data
conn, status := tt.setupConnection()
enrichedConnections := make(map[networkConnIndicator]timestamp.MicroTS)
enrichedConnections := make(map[indicator.NetworkConn]timestamp.MicroTS)

// Execute the enrichment
result, reason := m.enrichConnection(timestamp.Now(), conn, status, enrichedConnections)
Expand Down Expand Up @@ -290,14 +291,16 @@ func TestEnrichContainerEndpoint_EdgeCases(t *testing.T) {
expectedResultNG EnrichmentResult
expectedResultPLOP EnrichmentResult
expectedReasonNG EnrichmentReasonEp
prePopulateData func(*testing.T, map[containerEndpointIndicator]timestamp.MicroTS, map[processListeningIndicator]timestamp.MicroTS)
prePopulateData func(*testing.T,
map[indicator.ContainerEndpoint]timestamp.MicroTS,
map[indicator.ProcessListening]timestamp.MicroTS)
}{
"Fresh endpoint with no process info should yield result EnrichmentResultSuccess for Network Graph and EnrichmentResultInvalidInput for PLOP": {
setupEndpoint: func() (*containerEndpoint, *connStatus) {
ep := &containerEndpoint{
endpoint: commonEndpoint,
containerID: "test-container",
processKey: processInfo{}, // empty process info
processKey: indicator.ProcessInfo{}, // empty process info
}
return ep, freshConnStatus
},
Expand All @@ -323,14 +326,30 @@ func TestEnrichContainerEndpoint_EdgeCases(t *testing.T) {
expectedResultNG: EnrichmentResultSuccess,
expectedResultPLOP: EnrichmentResultSuccess,
expectedReasonNG: EnrichmentReasonEpDuplicate,
prePopulateData: func(t *testing.T, enrichedEndpoints map[containerEndpointIndicator]timestamp.MicroTS, processesListening map[processListeningIndicator]timestamp.MicroTS) {
prePopulateData: func(t *testing.T, enrichedEndpoints map[indicator.ContainerEndpoint]timestamp.MicroTS,
processesListening map[indicator.ProcessListening]timestamp.MicroTS) {
// Pre-populate with newer timestamp to trigger duplicate detection
indicator := containerEndpointIndicator{
entity: networkgraph.EntityForDeployment("test-deployment"),
port: 80,
protocol: net.TCP.ToProtobuf(),
endpointIndicator := indicator.ContainerEndpoint{
Entity: networkgraph.EntityForDeployment("test-deployment"),
Port: 80,
Protocol: net.TCP.ToProtobuf(),
}
enrichedEndpoints[indicator] = timestamp.Now() // newer timestamp
enrichedEndpoints[endpointIndicator] = timestamp.Now() // newer timestamp
processIndicator := indicator.ProcessListening{
ContainerName: "test-container",
DeploymentID: "test-deployment",
Process: indicator.ProcessInfo{
ProcessName: "test-process",
ProcessArgs: "test-args",
ProcessExec: "test-exec",
},
Port: 80,
Protocol: net.TCP.ToProtobuf(),
PodID: "test-pod",
PodUID: "test-pod-uid",
Namespace: "test-namespace",
}
processesListening[processIndicator] = timestamp.Now() // newer timestamp
},
},
}
Expand All @@ -352,8 +371,8 @@ func TestEnrichContainerEndpoint_EdgeCases(t *testing.T) {

// Setup test data
ep, status := tt.setupEndpoint()
enrichedEndpoints := make(map[containerEndpointIndicator]timestamp.MicroTS)
processesListening := make(map[processListeningIndicator]timestamp.MicroTS)
enrichedEndpoints := make(map[indicator.ContainerEndpoint]timestamp.MicroTS)
processesListening := make(map[indicator.ProcessListening]timestamp.MicroTS)

// Pre-populate data if validation function needs it
if tt.prePopulateData != nil {
Expand Down
105 changes: 105 additions & 0 deletions sensor/common/networkflow/manager/indicator/indicator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package indicator

import (
"fmt"

"github.com/stackrox/rox/generated/storage"
"github.com/stackrox/rox/pkg/networkgraph"
"github.com/stackrox/rox/pkg/protoconv"
"github.com/stackrox/rox/pkg/timestamp"
)

// ProcessInfo represents process information used in indicators
type ProcessInfo struct {
ProcessName string
ProcessArgs string
ProcessExec string
}

func (p *ProcessInfo) String() string {
return fmt.Sprintf("%s: %s %s", p.ProcessExec, p.ProcessName, p.ProcessArgs)
}

// NetworkConn represents a network connection.
// Fields are sorted by their size to optimize for memory padding.
type NetworkConn struct {
SrcEntity networkgraph.Entity // ~38 bytes
DstEntity networkgraph.Entity // ~38 bytes
Protocol storage.L4Protocol // 4 bytes
DstPort uint16 // 2 bytes
}

func (i *NetworkConn) ToProto(ts timestamp.MicroTS) *storage.NetworkFlow {
proto := &storage.NetworkFlow{
Props: &storage.NetworkFlowProperties{
SrcEntity: i.SrcEntity.ToProto(),
DstEntity: i.DstEntity.ToProto(),
DstPort: uint32(i.DstPort),
L4Protocol: i.Protocol,
},
}

if ts != timestamp.InfiniteFuture {
proto.LastSeenTimestamp = protoconv.ConvertMicroTSToProtobufTS(ts)
}
return proto
}

// ContainerEndpoint is a key in Sensor's maps that track active endpoints. It's set of fields should be minimal.
// Fields are sorted by their size to optimize for memory padding.
type ContainerEndpoint struct {
Entity networkgraph.Entity // ~38 bytes
Protocol storage.L4Protocol // 4 bytes
Port uint16 // 2 bytes
}

func (i *ContainerEndpoint) ToProto(ts timestamp.MicroTS) *storage.NetworkEndpoint {
proto := &storage.NetworkEndpoint{
Props: &storage.NetworkEndpointProperties{
Entity: i.Entity.ToProto(),
Port: uint32(i.Port),
L4Protocol: i.Protocol,
},
}

if ts != timestamp.InfiniteFuture {
proto.LastActiveTimestamp = protoconv.ConvertMicroTSToProtobufTS(ts)
}
return proto
}

// ProcessListening represents a listening process.
// Fields are sorted by their size to optimize for memory padding.
type ProcessListening struct {
Process ProcessInfo // 48 bytes (3 strings)
PodID string // 16 bytes
ContainerName string // 16 bytes
DeploymentID string // 16 bytes
PodUID string // 16 bytes
Namespace string // 16 bytes
Protocol storage.L4Protocol // 4 bytes
Port uint16 // 2 bytes
}

func (i *ProcessListening) ToProto(ts timestamp.MicroTS) *storage.ProcessListeningOnPortFromSensor {
proto := &storage.ProcessListeningOnPortFromSensor{
Port: uint32(i.Port),
Protocol: i.Protocol,
Process: &storage.ProcessIndicatorUniqueKey{
PodId: i.PodID,
ContainerName: i.ContainerName,
ProcessName: i.Process.ProcessName,
ProcessExecFilePath: i.Process.ProcessExec,
ProcessArgs: i.Process.ProcessArgs,
},
DeploymentId: i.DeploymentID,
PodUid: i.PodUID,
Namespace: i.Namespace,
}

if ts != timestamp.InfiniteFuture {
proto.CloseTimestamp = protoconv.ConvertMicroTSToProtobufTS(ts)
}

return proto
}
33 changes: 17 additions & 16 deletions sensor/common/networkflow/manager/manager_enrich_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/stackrox/rox/pkg/timestamp"
"github.com/stackrox/rox/sensor/common/centralcaps"
"github.com/stackrox/rox/sensor/common/clusterentities"
"github.com/stackrox/rox/sensor/common/networkflow/manager/indicator"
flowMetrics "github.com/stackrox/rox/sensor/common/networkflow/metrics"
)

Expand All @@ -22,7 +23,7 @@ func (m *networkFlowManager) executeConnectionAction(
conn *connection,
status *connStatus,
hostConns *hostConnections,
enrichedConnections map[networkConnIndicator]timestamp.MicroTS,
enrichedConnections map[indicator.NetworkConn]timestamp.MicroTS,
now timestamp.MicroTS,
) {
switch action {
Expand All @@ -47,7 +48,7 @@ func (m *networkFlowManager) executeConnectionAction(
}
}

func (m *networkFlowManager) enrichHostConnections(now timestamp.MicroTS, hostConns *hostConnections, enrichedConnections map[networkConnIndicator]timestamp.MicroTS) {
func (m *networkFlowManager) enrichHostConnections(now timestamp.MicroTS, hostConns *hostConnections, enrichedConnections map[indicator.NetworkConn]timestamp.MicroTS) {
hostConns.mutex.Lock()
defer hostConns.mutex.Unlock()

Expand All @@ -63,7 +64,7 @@ func (m *networkFlowManager) enrichHostConnections(now timestamp.MicroTS, hostCo
// enrichConnection updates `enrichedConnections` and `m.activeConnections`.
// It returns the enrichment result and provides reason for returning such result.
// Additionally, it sets the outcome in the `status` field to reflect the outcome of the enrichment in memory-efficient way by avoiding copying.
func (m *networkFlowManager) enrichConnection(now timestamp.MicroTS, conn *connection, status *connStatus, enrichedConnections map[networkConnIndicator]timestamp.MicroTS) (EnrichmentResult, EnrichmentReasonConn) {
func (m *networkFlowManager) enrichConnection(now timestamp.MicroTS, conn *connection, status *connStatus, enrichedConnections map[indicator.NetworkConn]timestamp.MicroTS) (EnrichmentResult, EnrichmentReasonConn) {
isFresh := status.isFresh(now)

// Use shared container resolution logic
Expand Down Expand Up @@ -199,33 +200,33 @@ func (m *networkFlowManager) enrichConnection(now timestamp.MicroTS, conn *conne

for _, lookupResult := range lookupResults {
for _, port := range lookupResult.ContainerPorts {
indicator := networkConnIndicatorWithAge{
networkConnIndicator: networkConnIndicator{
dstPort: port,
protocol: conn.remote.L4Proto.ToProtobuf(),
ind := networkConnIndicatorWithAge{
NetworkConn: indicator.NetworkConn{
DstPort: port,
Protocol: conn.remote.L4Proto.ToProtobuf(),
},
lastUpdate: now,
}

if conn.incoming {
indicator.srcEntity = lookupResult.Entity
indicator.dstEntity = networkgraph.EntityForDeployment(container.DeploymentID)
ind.SrcEntity = lookupResult.Entity
ind.DstEntity = networkgraph.EntityForDeployment(container.DeploymentID)
} else {
indicator.srcEntity = networkgraph.EntityForDeployment(container.DeploymentID)
indicator.dstEntity = lookupResult.Entity
ind.SrcEntity = networkgraph.EntityForDeployment(container.DeploymentID)
ind.DstEntity = lookupResult.Entity
}

// Multiple connections from a collector can result in a single enriched connection
// hence update the timestamp only if we have a more recent connection than the one we have already enriched.
if oldTS, found := enrichedConnections[indicator.networkConnIndicator]; !found || oldTS < status.lastSeen {
enrichedConnections[indicator.networkConnIndicator] = status.lastSeen
if oldTS, found := enrichedConnections[ind.NetworkConn]; !found || oldTS < status.lastSeen {
enrichedConnections[ind.NetworkConn] = status.lastSeen
if !features.SensorCapturesIntermediateEvents.Enabled() {
continue
}

concurrency.WithLock(&m.activeConnectionsMutex, func() {
if !status.isClosed() {
m.activeConnections[*conn] = &indicator
m.activeConnections[*conn] = &ind
flowMetrics.SetActiveFlowsTotalGauge(len(m.activeConnections))
return
}
Expand Down Expand Up @@ -309,7 +310,7 @@ func (m *networkFlowManager) handleConnectionEnrichmentResult(result EnrichmentR
// Returns true when connection was removed from activeConnections, and false if not found within activeConnections.
func deactivateConnectionNoLock(conn *connection,
activeConnections map[connection]*networkConnIndicatorWithAge,
enrichedConnections map[networkConnIndicator]timestamp.MicroTS,
enrichedConnections map[indicator.NetworkConn]timestamp.MicroTS,
now timestamp.MicroTS,
) bool {
activeConn, found := activeConnections[*conn]
Expand All @@ -319,7 +320,7 @@ func deactivateConnectionNoLock(conn *connection,
}
// Active connection found - mark that Sensor considers this connection no longer active
// due to missing data about the container.
enrichedConnections[activeConn.networkConnIndicator] = now
enrichedConnections[activeConn.NetworkConn] = now
delete(activeConnections, *conn)
flowMetrics.SetActiveFlowsTotalGauge(len(activeConnections))
return true
Expand Down
Loading
Loading