Skip to content

Commit b4297f3

Browse files
authored
Merge pull request #422 from kubescape/feature/cloud-services
Feature/cloud services
2 parents 6259246 + 2269795 commit b4297f3

File tree

19 files changed

+487
-254
lines changed

19 files changed

+487
-254
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ module github.com/kubescape/node-agent
33
go 1.23.0
44

55
require (
6+
github.com/armosec/armoapi-go v0.0.484
67
github.com/DmitriyVTitov/size v1.5.0
78
github.com/anchore/syft v1.13.0
89
github.com/aquilax/truncate v1.0.0
9-
github.com/armosec/armoapi-go v0.0.470
1010
github.com/armosec/utils-k8s-go v0.0.30
1111
github.com/cenkalti/backoff/v4 v4.3.0
1212
github.com/cilium/ebpf v0.16.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj
150150
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
151151
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
152152
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
153-
github.com/armosec/armoapi-go v0.0.470 h1:fT2J7SruNvOR1Q8RQXjiZ0JvNtJxjVUx68rl0X4leFU=
154-
github.com/armosec/armoapi-go v0.0.470/go.mod h1:TruqDSAPgfRBXCeM+Cgp6nN4UhJSbe7la+XDKV2pTsY=
153+
github.com/armosec/armoapi-go v0.0.484 h1:ALW+bND5ZAKeUa/000hcv2XDVVbawPo9hL4NEEliQVM=
154+
github.com/armosec/armoapi-go v0.0.484/go.mod h1:TruqDSAPgfRBXCeM+Cgp6nN4UhJSbe7la+XDKV2pTsY=
155155
github.com/armosec/gojay v1.2.17 h1:VSkLBQzD1c2V+FMtlGFKqWXNsdNvIKygTKJI9ysY8eM=
156156
github.com/armosec/gojay v1.2.17/go.mod h1:vuvX3DlY0nbVrJ0qCklSS733AWMoQboq3cFyuQW9ybc=
157157
github.com/armosec/utils-go v0.0.57 h1:0RaqexK+t7HeKWfldBv2C1JiLLGuUx9FP0DGWDNRJpg=

main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import (
2323
"github.com/kubescape/node-agent/pkg/cloudmetadata"
2424
"github.com/kubescape/node-agent/pkg/config"
2525
"github.com/kubescape/node-agent/pkg/containerwatcher/v1"
26-
"github.com/kubescape/node-agent/pkg/eventreporters/dnsmanager"
26+
"github.com/kubescape/node-agent/pkg/dnsmanager"
2727
"github.com/kubescape/node-agent/pkg/exporters"
2828
"github.com/kubescape/node-agent/pkg/filehandler/v1"
2929
"github.com/kubescape/node-agent/pkg/healthmanager"
@@ -247,7 +247,7 @@ func main() {
247247
exporter := exporters.InitExporters(cfg.Exporters, clusterData.ClusterName, cfg.NodeName, cloudMetadata)
248248

249249
// create runtimeDetection managers
250-
ruleManager, err = rulemanagerv1.CreateRuleManager(ctx, cfg, k8sClient, ruleBindingCache, objCache, exporter, prometheusExporter, cfg.NodeName, clusterData.ClusterName, processManager)
250+
ruleManager, err = rulemanagerv1.CreateRuleManager(ctx, cfg, k8sClient, ruleBindingCache, objCache, exporter, prometheusExporter, cfg.NodeName, clusterData.ClusterName, processManager, dnsResolver)
251251
if err != nil {
252252
logger.L().Ctx(ctx).Fatal("error creating RuleManager", helpers.Error(err))
253253
}

pkg/containerwatcher/v1/container_watcher.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/kubescape/node-agent/pkg/applicationprofilemanager"
2929
"github.com/kubescape/node-agent/pkg/config"
3030
"github.com/kubescape/node-agent/pkg/containerwatcher"
31+
"github.com/kubescape/node-agent/pkg/dnsmanager"
3132
tracerhardlink "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/tracer"
3233
tracerhardlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/hardlink/types"
3334
tracerhttp "github.com/kubescape/node-agent/pkg/ebpf/gadgets/http/tracer"
@@ -40,7 +41,6 @@ import (
4041
tracersshtype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/ssh/types"
4142
tracersymlink "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/tracer"
4243
tracersymlinktype "github.com/kubescape/node-agent/pkg/ebpf/gadgets/symlink/types"
43-
"github.com/kubescape/node-agent/pkg/eventreporters/dnsmanager"
4444
"github.com/kubescape/node-agent/pkg/eventreporters/rulepolicy"
4545
"github.com/kubescape/node-agent/pkg/malwaremanager"
4646
"github.com/kubescape/node-agent/pkg/metricsmanager"

pkg/containerwatcher/v1/container_watcher_private.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ func (ch *IGContainerWatcher) startContainerCollection(ctx context.Context) erro
9292
ch.ruleManager.ContainerCallback,
9393
ch.sbomManager.ContainerCallback,
9494
ch.processManager.ContainerCallback,
95+
ch.dnsManager.ContainerCallback,
9596
}
9697

9798
for receiver := range ch.thirdPartyContainerReceivers.Iter() {

pkg/dnsmanager/dns_manager.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package dnsmanager
2+
3+
import (
4+
"net"
5+
"strings"
6+
"time"
7+
8+
mapset "github.com/deckarep/golang-set/v2"
9+
"github.com/goradd/maps"
10+
containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection"
11+
tracerdnstype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/dns/types"
12+
"istio.io/pkg/cache"
13+
)
14+
15+
// DNSManager is used to manage DNS events and save IP resolutions.
16+
type DNSManager struct {
17+
addressToDomainMap maps.SafeMap[string, string]
18+
lookupCache cache.ExpiringCache // Cache for DNS lookups
19+
failureCache cache.ExpiringCache // Cache for failed lookups
20+
containerToCloudServices maps.SafeMap[string, mapset.Set[string]] // key: containerId, value: set of cloud services
21+
}
22+
23+
type cacheEntry struct {
24+
addresses []string
25+
}
26+
27+
const (
28+
defaultPositiveTTL = 1 * time.Minute // Default TTL for successful lookups
29+
defaultNegativeTTL = 5 * time.Second // Default TTL for failed lookups
30+
maxServiceCacheSize = 50 // Maximum number of cloud services to cache per container
31+
)
32+
33+
var _ DNSManagerClient = (*DNSManager)(nil)
34+
var _ DNSResolver = (*DNSManager)(nil)
35+
36+
func CreateDNSManager() *DNSManager {
37+
return &DNSManager{
38+
// Create TTL caches with their respective expiration times
39+
lookupCache: cache.NewTTL(defaultPositiveTTL, defaultPositiveTTL),
40+
failureCache: cache.NewTTL(defaultNegativeTTL, defaultNegativeTTL),
41+
}
42+
}
43+
44+
func (dm *DNSManager) ContainerCallback(notif containercollection.PubSubEvent) {
45+
switch notif.Type {
46+
case containercollection.EventTypeAddContainer:
47+
dm.containerToCloudServices.Set(notif.Container.Runtime.ContainerID, mapset.NewSet[string]())
48+
case containercollection.EventTypeRemoveContainer:
49+
dm.containerToCloudServices.Delete(notif.Container.Runtime.ContainerID)
50+
}
51+
}
52+
53+
func (dm *DNSManager) ReportEvent(dnsEvent tracerdnstype.Event) {
54+
if isCloudService(dnsEvent.DNSName) {
55+
if dm.containerToCloudServices.Has(dnsEvent.Runtime.ContainerID) {
56+
// Guard against cache size getting too large by checking the cardinality per container
57+
if dm.containerToCloudServices.Get(dnsEvent.Runtime.ContainerID).Cardinality() < maxServiceCacheSize {
58+
dm.containerToCloudServices.Get(dnsEvent.Runtime.ContainerID).Add(dnsEvent.DNSName)
59+
}
60+
}
61+
}
62+
63+
if len(dnsEvent.Addresses) > 0 {
64+
for _, address := range dnsEvent.Addresses {
65+
dm.addressToDomainMap.Set(address, dnsEvent.DNSName)
66+
}
67+
68+
// Update the cache with these known good addresses
69+
dm.lookupCache.Set(dnsEvent.DNSName, cacheEntry{
70+
addresses: dnsEvent.Addresses,
71+
})
72+
return
73+
}
74+
75+
// Check if we've recently failed to look up this domain
76+
if _, found := dm.failureCache.Get(dnsEvent.DNSName); found {
77+
return
78+
}
79+
80+
// Check if we have a cached result
81+
if cached, found := dm.lookupCache.Get(dnsEvent.DNSName); found {
82+
entry := cached.(cacheEntry)
83+
// Use cached addresses
84+
for _, addr := range entry.addresses {
85+
dm.addressToDomainMap.Set(addr, dnsEvent.DNSName)
86+
}
87+
return
88+
}
89+
90+
// Only perform lookup if we don't have cached results
91+
addresses, err := net.LookupIP(dnsEvent.DNSName)
92+
if err != nil {
93+
// Cache the failure - we just need to store something, using empty struct
94+
dm.failureCache.Set(dnsEvent.DNSName, struct{}{})
95+
return
96+
}
97+
98+
// Convert addresses to strings and store them
99+
addrStrings := make([]string, 0, len(addresses))
100+
for _, addr := range addresses {
101+
addrStr := addr.String()
102+
addrStrings = append(addrStrings, addrStr)
103+
dm.addressToDomainMap.Set(addrStr, dnsEvent.DNSName)
104+
}
105+
106+
// Cache the successful lookup
107+
dm.lookupCache.Set(dnsEvent.DNSName, cacheEntry{
108+
addresses: addrStrings,
109+
})
110+
}
111+
112+
func (dm *DNSManager) ResolveIPAddress(ipAddr string) (string, bool) {
113+
domain := dm.addressToDomainMap.Get(ipAddr)
114+
return domain, domain != ""
115+
}
116+
117+
func (dm *DNSManager) ResolveContainerToCloudServices(containerId string) mapset.Set[string] {
118+
if services, found := dm.containerToCloudServices.Load(containerId); found {
119+
return services
120+
}
121+
return nil
122+
}
123+
124+
func isCloudService(domain string) bool {
125+
domain = strings.ToLower(domain)
126+
// Common cloud service domains
127+
awsDomains := []string{
128+
"amazonaws.com.",
129+
"cloudfront.net.",
130+
"aws.amazon.com.",
131+
"elasticbeanstalk.com.",
132+
}
133+
134+
azureDomains := []string{
135+
"azure.com.",
136+
"azurewebsites.net.",
137+
"cloudapp.net.",
138+
"azure-api.net.",
139+
}
140+
141+
gcpDomains := []string{
142+
"googleapis.com.",
143+
"appspot.com.",
144+
"cloudfunctions.net.",
145+
"run.app.",
146+
}
147+
148+
// Combine all cloud domains
149+
allCloudDomains := append(awsDomains, azureDomains...)
150+
allCloudDomains = append(allCloudDomains, gcpDomains...)
151+
152+
// Check if the input domain ends with any of the cloud domains
153+
for _, cloudDomain := range allCloudDomains {
154+
if strings.HasSuffix(domain, cloudDomain) {
155+
return true
156+
}
157+
}
158+
159+
return false
160+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package dnsmanager
22

33
import (
4+
mapset "github.com/deckarep/golang-set/v2"
5+
containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection"
46
tracerdnstype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/dns/types"
57
)
68

79
type DNSManagerClient interface {
810
ReportEvent(networkEvent tracerdnstype.Event)
11+
ContainerCallback(notif containercollection.PubSubEvent)
912
}
1013

1114
type DNSResolver interface {
1215
ResolveIPAddress(ipAddr string) (string, bool)
16+
ResolveContainerToCloudServices(containerId string) mapset.Set[string]
1317
}

pkg/eventreporters/dnsmanager/dns_manager_mock.go renamed to pkg/dnsmanager/dns_manager_mock.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package dnsmanager
22

33
import (
4+
mapset "github.com/deckarep/golang-set/v2"
5+
containercollection "github.com/inspektor-gadget/inspektor-gadget/pkg/container-collection"
46
tracerdnstype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/dns/types"
57
)
68

@@ -17,6 +19,13 @@ func CreateDNSManagerMock() *DNSManagerMock {
1719
func (n *DNSManagerMock) ReportEvent(_ tracerdnstype.Event) {
1820
}
1921

22+
func (n *DNSManagerMock) ContainerCallback(notif containercollection.PubSubEvent) {
23+
}
24+
2025
func (n *DNSManagerMock) ResolveIPAddress(_ string) (string, bool) {
2126
return "", false
2227
}
28+
29+
func (n *DNSManagerMock) ResolveContainerToCloudServices(_ string) mapset.Set[string] {
30+
return nil
31+
}

pkg/eventreporters/dnsmanager/dns_manager_test.go renamed to pkg/dnsmanager/dns_manager_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package dnsmanager
22

33
import (
44
"net"
5+
"strings"
56
"sync"
67
"testing"
78

@@ -233,3 +234,72 @@ func TestConcurrentAccess(t *testing.T) {
233234
}
234235
}
235236
}
237+
238+
func TestIsCloudService(t *testing.T) {
239+
tests := []struct {
240+
name string
241+
domain string
242+
expected bool
243+
}{
244+
// AWS tests
245+
{"AWS EC2", "ec2.amazonaws.com.", true},
246+
{"AWS S3", "mybucket.s3.amazonaws.com.", true},
247+
{"AWS CloudFront", "d1234.cloudfront.net.", true},
248+
{"AWS Console", "console.aws.amazon.com.", true},
249+
{"AWS Elastic Beanstalk", "myapp.elasticbeanstalk.com.", true},
250+
251+
// Azure tests
252+
{"Azure Web App", "myapp.azurewebsites.net.", true},
253+
{"Azure Cloud App", "myservice.cloudapp.net.", true},
254+
{"Azure API", "api.azure-api.net.", true},
255+
{"Azure Portal", "portal.azure.com.", true},
256+
257+
// GCP tests
258+
{"Google APIs", "storage.googleapis.com.", true},
259+
{"App Engine", "myapp.appspot.com.", true},
260+
{"Cloud Functions", "function.cloudfunctions.net.", true},
261+
{"Cloud Run", "myservice.run.app.", true},
262+
263+
// Negative tests
264+
{"Regular Domain", "example.com.", false},
265+
{"Subdomain", "sub.example.com.", false},
266+
{"Empty String", "", false},
267+
{"Single Dot", ".", false},
268+
{"Similar But Not Cloud", "notamazonsaws.com.", false},
269+
// {"Non Cloud With Azure In Name", "fake-azure.com.", false}, // Because of cpu usage we keep the check "simple".
270+
271+
// Edge cases
272+
{"Domain Without Final Dot", "example.amazonaws.com", false},
273+
{"Multiple Dots", "my.app.amazonaws.com.", true},
274+
{"Uppercase Domain", "MYAPP.AMAZONAWS.COM.", true},
275+
{"Mixed Case Domain", "MyApp.AmAzOnAwS.cOm.", true},
276+
}
277+
278+
for _, tt := range tests {
279+
t.Run(tt.name, func(t *testing.T) {
280+
result := isCloudService(strings.ToLower(tt.domain)) // Convert input to lowercase
281+
if result != tt.expected {
282+
t.Errorf("isCloudService(%q) = %v; want %v",
283+
tt.domain, result, tt.expected)
284+
}
285+
})
286+
}
287+
}
288+
289+
// Benchmark function remains the same
290+
func BenchmarkIsCloudService(b *testing.B) {
291+
testDomains := []string{
292+
"ec2.amazonaws.com.",
293+
"example.com.",
294+
"myapp.azurewebsites.net.",
295+
"storage.googleapis.com.",
296+
"notacloud.com.",
297+
}
298+
299+
b.ResetTimer()
300+
for i := 0; i < b.N; i++ {
301+
for _, domain := range testDomains {
302+
isCloudService(domain)
303+
}
304+
}
305+
}

0 commit comments

Comments
 (0)