Skip to content
Merged
2 changes: 1 addition & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func main() {
if cfg.EnablePrometheusExporter {
prometheusExporter = metricprometheus.NewPrometheusMetric()
} else {
prometheusExporter = metricsmanager.NewMetricsMock()
prometheusExporter = metricsmanager.NewMetricsNoop()
}

// Create watchers
Expand Down
21 changes: 4 additions & 17 deletions pkg/containerwatcher/v2/tracers/httpparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,6 @@ import (
"github.com/kubescape/storage/pkg/apis/softwarecomposition/consts"
)

var writeSyscalls = map[string]bool{
"write": true,
"writev": true,
"sendto": true,
"sendmsg": true,
}

var readSyscalls = map[string]bool{
"read": true,
"readv": true,
"recvfrom": true,
"recvmsg": true,
}

var ConsistentHeaders = []string{
"Accept-Encoding",
"Accept-Language",
Expand Down Expand Up @@ -69,11 +55,12 @@ func ExtractConsistentHeaders(headers http.Header) map[string][]string {
}

func GetPacketDirection(syscall string) (consts.NetworkDirection, error) {
if readSyscalls[syscall] {
switch syscall {
case "read", "readv", "recvfrom", "recvmsg":
return consts.Inbound, nil
} else if writeSyscalls[syscall] {
case "write", "writev", "sendto", "sendmsg":
return consts.Outbound, nil
} else {
default:
return "", fmt.Errorf("unknown syscall %s", syscall)
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/metricsmanager/metrics_manager_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type MetricsManager interface {
ReportEvent(eventType utils.EventType)
ReportFailedEvent()
ReportRuleProcessed(ruleID string)
ReportRulePrefiltered(ruleName string)
ReportRuleAlert(ruleID string)
ReportRuleEvaluationTime(ruleID string, eventType utils.EventType, duration time.Duration)
//ReportEbpfStats(stats *top.Event[toptypes.Stats])
Expand Down
2 changes: 2 additions & 0 deletions pkg/metricsmanager/metrics_manager_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func (m *MetricsMock) ReportRuleEvaluationTime(ruleID string, eventType utils.Ev
//func (m *MetricsMock) ReportEbpfStats(stats *top.Event[toptypes.Stats]) {
//}

func (m *MetricsMock) ReportRulePrefiltered(ruleName string) {}

func (m *MetricsMock) ReportContainerStart() {}

func (m *MetricsMock) ReportContainerStop() {}
23 changes: 23 additions & 0 deletions pkg/metricsmanager/metrics_manager_noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package metricsmanager

import (
"time"

"github.com/kubescape/node-agent/pkg/utils"
)

var _ MetricsManager = (*MetricsNoop)(nil)

type MetricsNoop struct{}

func NewMetricsNoop() *MetricsNoop { return &MetricsNoop{} }
func (m *MetricsNoop) Start() {}
func (m *MetricsNoop) Destroy() {}
func (m *MetricsNoop) ReportEvent(_ utils.EventType) {}
func (m *MetricsNoop) ReportFailedEvent() {}
func (m *MetricsNoop) ReportRuleProcessed(_ string) {}
func (m *MetricsNoop) ReportRulePrefiltered(_ string) {}
func (m *MetricsNoop) ReportRuleAlert(_ string) {}
func (m *MetricsNoop) ReportRuleEvaluationTime(_ string, _ utils.EventType, _ time.Duration) {}
func (m *MetricsNoop) ReportContainerStart() {}
func (m *MetricsNoop) ReportContainerStop() {}
45 changes: 39 additions & 6 deletions pkg/metricsmanager/prometheus/prometheus.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ type PrometheusMetric struct {
ebpfKmodCounter prometheus.Counter
ebpfUnshareCounter prometheus.Counter
ebpfBpfCounter prometheus.Counter
ruleCounter *prometheus.CounterVec
alertCounter *prometheus.CounterVec
ruleCounter *prometheus.CounterVec
rulePrefilteredCounter *prometheus.CounterVec
alertCounter *prometheus.CounterVec
ruleEvaluationTime *prometheus.HistogramVec

// Program ID metrics
Expand All @@ -60,8 +61,9 @@ type PrometheusMetric struct {
containerStopCounter prometheus.Counter

// Cache to avoid allocating Labels maps on every call
ruleCounterCache map[string]prometheus.Counter
alertCounterCache map[string]prometheus.Counter
ruleCounterCache map[string]prometheus.Counter
rulePrefilteredCounterCache map[string]prometheus.Counter
alertCounterCache map[string]prometheus.Counter
counterCacheMutex sync.RWMutex
}

Expand Down Expand Up @@ -139,6 +141,10 @@ func NewPrometheusMetric() *PrometheusMetric {
Name: "node_agent_rule_counter",
Help: "The total number of rules processed by the engine",
}, []string{prometheusRuleIdLabel}),
rulePrefilteredCounter: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "node_agent_rule_prefiltered_total",
Help: "Total number of rule evaluations skipped by pre-filter",
}, []string{prometheusRuleIdLabel}),
alertCounter: promauto.NewCounterVec(prometheus.CounterOpts{
Name: "node_agent_alert_counter",
Help: "The total number of alerts sent by the engine",
Expand Down Expand Up @@ -201,8 +207,9 @@ func NewPrometheusMetric() *PrometheusMetric {
}),

// Initialize counter caches
ruleCounterCache: make(map[string]prometheus.Counter),
alertCounterCache: make(map[string]prometheus.Counter),
ruleCounterCache: make(map[string]prometheus.Counter),
rulePrefilteredCounterCache: make(map[string]prometheus.Counter),
alertCounterCache: make(map[string]prometheus.Counter),
}
}

Expand All @@ -225,6 +232,7 @@ func (p *PrometheusMetric) Destroy() {
prometheus.Unregister(p.ebpfRandomXCounter)
prometheus.Unregister(p.ebpfFailedCounter)
prometheus.Unregister(p.ruleCounter)
prometheus.Unregister(p.rulePrefilteredCounter)
prometheus.Unregister(p.alertCounter)
prometheus.Unregister(p.ruleEvaluationTime)
prometheus.Unregister(p.ebpfSymlinkCounter)
Expand Down Expand Up @@ -342,6 +350,31 @@ func (p *PrometheusMetric) ReportRuleProcessed(ruleID string) {
p.getCachedRuleCounter(ruleID).Inc()
}

func (p *PrometheusMetric) getCachedRulePrefilteredCounter(ruleName string) prometheus.Counter {
p.counterCacheMutex.RLock()
counter, exists := p.rulePrefilteredCounterCache[ruleName]
p.counterCacheMutex.RUnlock()

if exists {
return counter
}

p.counterCacheMutex.Lock()
defer p.counterCacheMutex.Unlock()

if counter, exists := p.rulePrefilteredCounterCache[ruleName]; exists {
return counter
}

counter = p.rulePrefilteredCounter.With(prometheus.Labels{prometheusRuleIdLabel: ruleName})
p.rulePrefilteredCounterCache[ruleName] = counter
return counter
}

func (p *PrometheusMetric) ReportRulePrefiltered(ruleName string) {
p.getCachedRulePrefilteredCounter(ruleName).Inc()
}

func (p *PrometheusMetric) ReportRuleAlert(ruleID string) {
p.getCachedAlertCounter(ruleID).Inc()
}
Expand Down
6 changes: 6 additions & 0 deletions pkg/rulebindingmanager/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/kubescape/node-agent/pkg/k8sclient"
"github.com/kubescape/node-agent/pkg/rulebindingmanager"
typesv1 "github.com/kubescape/node-agent/pkg/rulebindingmanager/types/v1"
"github.com/kubescape/node-agent/pkg/rulemanager/prefilter"
"github.com/kubescape/node-agent/pkg/rulemanager/rulecreator"
rulemanagertypesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1"
"github.com/kubescape/node-agent/pkg/utils"
Expand Down Expand Up @@ -400,14 +401,19 @@ func (c *RBCache) createRules(rulesForPod []typesv1.RuntimeAlertRuleBindingRule)
func (c *RBCache) createRule(r *typesv1.RuntimeAlertRuleBindingRule) []rulemanagertypesv1.Rule {
if r.RuleID != "" {
rule := c.ruleCreator.CreateRuleByID(r.RuleID)
rule.Prefilter = prefilter.ParseWithDefaults(rule.State, r.Parameters)
return []rulemanagertypesv1.Rule{rule}
}
if r.RuleName != "" {
rule := c.ruleCreator.CreateRuleByName(r.RuleName)
rule.Prefilter = prefilter.ParseWithDefaults(rule.State, r.Parameters)
return []rulemanagertypesv1.Rule{rule}
}
if len(r.RuleTags) > 0 {
rules := c.ruleCreator.CreateRulesByTags(r.RuleTags)
for i := range rules {
rules[i].Prefilter = prefilter.ParseWithDefaults(rules[i].State, r.Parameters)
}
return rules
}

Expand Down
44 changes: 44 additions & 0 deletions pkg/rulebindingmanager/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
typesv1 "github.com/kubescape/node-agent/pkg/rulebindingmanager/types/v1"
rulemanagertypesv1 "github.com/kubescape/node-agent/pkg/rulemanager/types/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -1040,3 +1041,46 @@ func TestDiff(t *testing.T) {
})
}
}

func TestCreateRulePrefilter(t *testing.T) {
tests := []struct {
name string
binding *typesv1.RuntimeAlertRuleBindingRule
wantNil bool
wantIgnore []string
wantIncl []string
}{
{
name: "parameters propagate to prefilter",
binding: &typesv1.RuntimeAlertRuleBindingRule{
RuleID: "R0002",
Parameters: map[string]interface{}{
"ignorePrefixes": []interface{}{"/tmp", "/var/log"},
"includePrefixes": []interface{}{"/etc"},
},
},
wantIgnore: []string{"/tmp", "/var/log"},
wantIncl: []string{"/etc"},
},
{
name: "nil parameters produce nil prefilter",
binding: &typesv1.RuntimeAlertRuleBindingRule{RuleID: "R0002"},
wantNil: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewCacheMock("")
rules := c.createRule(tt.binding)
require.Len(t, rules, 1)
if tt.wantNil {
assert.Nil(t, rules[0].Prefilter)
} else {
require.NotNil(t, rules[0].Prefilter)
assert.Equal(t, tt.wantIgnore, rules[0].Prefilter.IgnorePrefixes)
assert.Equal(t, tt.wantIncl, rules[0].Prefilter.IncludePrefixes)
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ spec:
rules:
- ruleName: "Malicious SSH Connection"
parameters:
allowedPorts: [22, 2222]
ports: [22, 2222]
67 changes: 67 additions & 0 deletions pkg/rulemanager/extract_event_fields_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package rulemanager

import (
"net/http"
"testing"

"github.com/kubescape/node-agent/pkg/rulemanager/prefilter"
"github.com/kubescape/node-agent/pkg/utils"
"github.com/kubescape/storage/pkg/apis/softwarecomposition/consts"
"github.com/stretchr/testify/assert"
)

func TestExtractEventFields(t *testing.T) {
tests := []struct {
name string
event *utils.StructEvent
expect prefilter.EventFields
}{
{
name: "open event extracts path",
event: &utils.StructEvent{EventType: utils.OpenEventType, Path: "/etc/passwd"},
expect: prefilter.EventFields{Path: "/etc/passwd", Extracted: true},
},
{
name: "exec event extracts exe path",
event: &utils.StructEvent{EventType: utils.ExecveEventType, ExePath: "/usr/bin/curl"},
expect: prefilter.EventFields{Path: "/usr/bin/curl", Extracted: true},
},
{
name: "HTTP event extracts direction, method, port",
event: &utils.StructEvent{EventType: utils.HTTPEventType, Direction: consts.Inbound, DstPort: 8080, Request: &http.Request{Method: "POST"}},
expect: prefilter.EventFields{Dir: prefilter.DirInbound, MethodBit: prefilter.MethodPOST, DstPort: 8080, Extracted: true},
},
{
name: "HTTP nil request leaves method zero",
event: &utils.StructEvent{EventType: utils.HTTPEventType, Direction: consts.Outbound},
expect: prefilter.EventFields{Dir: prefilter.DirOutbound, Extracted: true},
},
{
name: "HTTP unknown direction maps to DirNone",
event: &utils.StructEvent{EventType: utils.HTTPEventType, Direction: "unknown", Request: &http.Request{Method: "POST"}},
expect: prefilter.EventFields{MethodBit: prefilter.MethodPOST, Extracted: true},
},
{
name: "network event extracts port and sets PortEligible",
event: &utils.StructEvent{EventType: utils.NetworkEventType, DstPort: 443},
expect: prefilter.EventFields{DstPort: 443, PortEligible: true, Extracted: true},
},
{
name: "SSH event extracts port and sets PortEligible",
event: &utils.StructEvent{EventType: utils.SSHEventType, DstPort: 22},
expect: prefilter.EventFields{DstPort: 22, PortEligible: true, Extracted: true},
},
{
name: "unhandled event type returns empty fields",
event: &utils.StructEvent{EventType: utils.DnsEventType},
expect: prefilter.EventFields{Extracted: true},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractEventFields(tt.event)
assert.Equal(t, tt.expect, got)
})
}
}
Loading
Loading