From 2c079662d375562e9bde245c2208bd54ff4b97d2 Mon Sep 17 00:00:00 2001 From: Vincent Boulineau <58430298+vboulineau@users.noreply.github.com> Date: Thu, 23 May 2024 18:22:07 +0200 Subject: [PATCH 01/32] Fix datadog-operator import (#25864) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 5b5b7a4d4f691a..627a09aafd164d 100644 --- a/go.mod +++ b/go.mod @@ -132,7 +132,7 @@ require ( github.com/DataDog/datadog-agent/pkg/util/pointer v0.54.0-rc.2 github.com/DataDog/datadog-agent/pkg/util/scrubber v0.54.0-rc.2 github.com/DataDog/datadog-go/v5 v5.5.0 - github.com/DataDog/datadog-operator v0.7.1-0.20240522074410-e0648ef62f13 + github.com/DataDog/datadog-operator v0.7.1-0.20240522081847-e83dd785258a github.com/DataDog/ebpf-manager v0.6.0 github.com/DataDog/gopsutil v1.2.2 github.com/DataDog/nikos v1.12.4 diff --git a/go.sum b/go.sum index 7cd2c61a082609..3b345994046620 100644 --- a/go.sum +++ b/go.sum @@ -755,8 +755,8 @@ github.com/DataDog/datadog-api-client-go/v2 v2.25.0/go.mod h1:QKOu6vscsh87fMY1lH github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go/v5 v5.5.0 h1:G5KHeB8pWBNXT4Jtw0zAkhdxEAWSpWH00geHI6LDrKU= github.com/DataDog/datadog-go/v5 v5.5.0/go.mod h1:K9kcYBlxkcPP8tvvjZZKs/m1edNAUFzBbdpTUKfCsuw= -github.com/DataDog/datadog-operator v0.7.1-0.20240522074410-e0648ef62f13 h1:Q5ucbyAaIdlGdagQtp5QS/aeRwtjC2ahWWsG0Kmtj5Q= -github.com/DataDog/datadog-operator v0.7.1-0.20240522074410-e0648ef62f13/go.mod h1:4C7T1SWCw8TmzXh19IqjLv3ZeCVeS5J3Zfht113+Ke4= +github.com/DataDog/datadog-operator v0.7.1-0.20240522081847-e83dd785258a h1:GO/7UZHWOivCTEsI5Yjf2l1ovqISEXJS2nm3sw4CjKg= +github.com/DataDog/datadog-operator v0.7.1-0.20240522081847-e83dd785258a/go.mod h1:4C7T1SWCw8TmzXh19IqjLv3ZeCVeS5J3Zfht113+Ke4= github.com/DataDog/dd-sensitive-data-scanner/sds-go/go v0.0.0-20240419161837-f1b2f553edfe h1:efzxujZ7VHWFxjmWjcJyUEpPrN8qdiZPYb+dBw547Wo= github.com/DataDog/dd-sensitive-data-scanner/sds-go/go v0.0.0-20240419161837-f1b2f553edfe/go.mod h1:TX7CTOQ3LbQjfAi4SwqUoR5gY1zfUk7VRBDTuArjaDc= github.com/DataDog/ebpf-manager v0.6.0 h1:7EpsQwa07+lObYyrVQ7AnSjHS1gkw1vgjVlbBvGDCTI= From a0db96c6cc45f52479f8d272d1156b901c6fb374 Mon Sep 17 00:00:00 2001 From: Usama Saqib Date: Thu, 23 May 2024 18:32:06 +0200 Subject: [PATCH 02/32] Retry KMT tests jobs on timeout (#25826) * retry on job timeout * move retry and timeout to sharedblock * fix retry count and move to shared block --- .gitlab/kernel_matrix_testing/common.yml | 11 +++++++++++ .gitlab/kernel_matrix_testing/security_agent.yml | 3 +-- .gitlab/kernel_matrix_testing/system_probe.yml | 3 +-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.gitlab/kernel_matrix_testing/common.yml b/.gitlab/kernel_matrix_testing/common.yml index c8da50818fd7dc..0cf1fa0f28ff78 100644 --- a/.gitlab/kernel_matrix_testing/common.yml +++ b/.gitlab/kernel_matrix_testing/common.yml @@ -160,6 +160,17 @@ # -- Test runners .kmt_run_tests: + retry: + max: 2 + when: + - job_execution_timeout + - runner_system_failure + - stuck_or_timeout_failure + - unknown_failure + - api_failure + - scheduler_failure + - stale_schedule + - data_integrity_failure variables: AWS_EC2_SSH_KEY_FILE: $CI_PROJECT_DIR/ssh_key RETRY: 2 diff --git a/.gitlab/kernel_matrix_testing/security_agent.yml b/.gitlab/kernel_matrix_testing/security_agent.yml index 6ba2d519a6b4db..9fd1067689a14f 100644 --- a/.gitlab/kernel_matrix_testing/security_agent.yml +++ b/.gitlab/kernel_matrix_testing/security_agent.yml @@ -105,6 +105,7 @@ upload_secagent_tests_arm64: allow_failure: true stage: kernel_matrix_testing_security_agent rules: !reference [.on_security_agent_changes_or_manual] + timeout: 1h 30m variables: TEST_COMPONENT: security-agent @@ -117,7 +118,6 @@ kmt_run_secagent_tests_x64: - kmt_setup_env_secagent_x64 - upload_dependencies_secagent_x64 - upload_secagent_tests_x64 - timeout: 3h variables: ARCH: "x86_64" parallel: @@ -158,7 +158,6 @@ kmt_run_secagent_tests_arm64: - kmt_setup_env_secagent_arm64 - upload_dependencies_secagent_arm64 - upload_secagent_tests_arm64 - timeout: 3h variables: ARCH: "arm64" parallel: diff --git a/.gitlab/kernel_matrix_testing/system_probe.yml b/.gitlab/kernel_matrix_testing/system_probe.yml index 4dd7013a94a185..b888248ce83ded 100644 --- a/.gitlab/kernel_matrix_testing/system_probe.yml +++ b/.gitlab/kernel_matrix_testing/system_probe.yml @@ -186,6 +186,7 @@ upload_sysprobe_tests_arm64: extends: .kmt_run_tests stage: kernel_matrix_testing_system_probe rules: !reference [.on_system_probe_or_e2e_changes_or_manual] + timeout: 1h 30m variables: TEST_COMPONENT: system-probe @@ -199,7 +200,6 @@ kmt_run_sysprobe_tests_x64: - upload_dependencies_sysprobe_x64 - upload_sysprobe_tests_x64 - upload_minimized_btfs_sysprobe_x64 - timeout: 3h variables: ARCH: "x86_64" parallel: @@ -236,7 +236,6 @@ kmt_run_sysprobe_tests_arm64: - upload_dependencies_sysprobe_arm64 - upload_sysprobe_tests_arm64 - upload_minimized_btfs_sysprobe_arm64 - timeout: 3h variables: ARCH: "arm64" parallel: From c16d645596963242ef1b3f25c54d374755ff6949 Mon Sep 17 00:00:00 2001 From: Andrea Marziali Date: Thu, 23 May 2024 19:12:49 +0200 Subject: [PATCH 03/32] add gunicorn service name detector (#25863) --- .../corechecks/servicediscovery/usm/python.go | 65 +++++++++++++++- .../servicediscovery/usm/service.go | 14 +++- .../servicediscovery/usm/service_test.go | 76 +++++++++++++++++++ 3 files changed, 149 insertions(+), 6 deletions(-) diff --git a/pkg/collector/corechecks/servicediscovery/usm/python.go b/pkg/collector/corechecks/servicediscovery/usm/python.go index 50f430699ae58e..443b48ffe26b9d 100644 --- a/pkg/collector/corechecks/servicediscovery/usm/python.go +++ b/pkg/collector/corechecks/servicediscovery/usm/python.go @@ -12,14 +12,24 @@ import ( ) const ( - initPy = "__init__.py" - allPyFiles = "*.py" + initPy = "__init__.py" + allPyFiles = "*.py" + gunicornEnvCmdArgs = "GUNICORN_CMD_ARGS" + wsgiAppEnv = "WSGI_APP" ) type pythonDetector struct { ctx DetectionContext } +type gunicornDetector struct { + ctx DetectionContext +} + +func newGunicornDetector(ctx DetectionContext) detector { + return &gunicornDetector{ctx: ctx} +} + func newPythonDetector(ctx DetectionContext) detector { return &pythonDetector{ctx: ctx} } @@ -98,3 +108,54 @@ func (p pythonDetector) findNearestTopLevel(fp string) string { } return path.Base(last) } + +func (g gunicornDetector) detect(args []string) (ServiceMetadata, bool) { + if fromEnv, ok := extractEnvVar(g.ctx.envs, gunicornEnvCmdArgs); ok { + name, ok := extractGunicornNameFrom(strings.Split(fromEnv, " ")) + if ok { + return NewServiceMetadata(name), true + } + } + if wsgiApp, ok := extractEnvVar(g.ctx.envs, wsgiAppEnv); ok && len(wsgiApp) > 0 { + return NewServiceMetadata(parseNameFromWsgiApp(wsgiApp)), true + } + + if name, ok := extractGunicornNameFrom(args); ok { + return NewServiceMetadata(name), true + } + return NewServiceMetadata("gunicorn"), true +} + +func extractGunicornNameFrom(args []string) (string, bool) { + skip, capture := false, false + for _, a := range args { + if capture { + return a, true + } + if skip { + skip = false + continue + } + if strings.HasPrefix(a, "-") { + if a == "-n" { + capture = true + continue + } + skip = !strings.ContainsRune(a, '=') + if skip { + continue + } + if value, ok := strings.CutPrefix(a, "--name="); ok { + return value, true + } + } else { + return parseNameFromWsgiApp(args[len(args)-1]), true + } + } + return "", false +} + +func parseNameFromWsgiApp(wsgiApp string) string { + name, _, _ := strings.Cut(wsgiApp, ":") + return name +} diff --git a/pkg/collector/corechecks/servicediscovery/usm/service.go b/pkg/collector/corechecks/servicediscovery/usm/service.go index ab9cdc83b9e46e..c66d65414118ef 100644 --- a/pkg/collector/corechecks/servicediscovery/usm/service.go +++ b/pkg/collector/corechecks/servicediscovery/usm/service.go @@ -92,14 +92,19 @@ func NewDetectionContext(logger *zap.Logger, args []string, envs []string, fs fs // workingDirFromEnvs returns the current working dir extracted from the PWD env func workingDirFromEnvs(envs []string) (string, bool) { - wd := "" + return extractEnvVar(envs, "PWD") +} + +func extractEnvVar(envs []string, name string) (string, bool) { + value := "" + prefix := name + "=" for _, v := range envs { - if strings.HasPrefix(v, "PWD=") { - _, wd, _ = strings.Cut(v, "=") + if strings.HasPrefix(v, prefix) { + _, value, _ = strings.Cut(v, "=") break } } - return wd, len(wd) > 0 + return value, len(value) > 0 } // abs returns the path itself if already absolute or the absolute path by joining cwd with path @@ -132,6 +137,7 @@ var binsWithContext = map[string]detectorCreatorFn{ "node": newNodeDetector, "dotnet": newDotnetDetector, "php": newPhpDetector, + "gunicorn": newGunicornDetector, } func checkForInjectionNaming(envs []string) bool { diff --git a/pkg/collector/corechecks/servicediscovery/usm/service_test.go b/pkg/collector/corechecks/servicediscovery/usm/service_test.go index 51f78fa2e5c801..7c56bc9adcd2a2 100644 --- a/pkg/collector/corechecks/servicediscovery/usm/service_test.go +++ b/pkg/collector/corechecks/servicediscovery/usm/service_test.go @@ -415,6 +415,82 @@ func TestExtractServiceMetadata(t *testing.T) { expectedServiceTag: "howdy", fromDDService: false, }, + { + name: "gunicorn simple", + cmdline: []string{ + "gunicorn", + "--workers=2", + "test:app", + }, + expectedServiceTag: "test", + }, + { + name: "gunicorn from name", + cmdline: []string{ + "gunicorn", + "--workers=2", + "-b", + "0.0.0.0", + "-n", + "dummy", + "test:app", + }, + expectedServiceTag: "dummy", + }, + { + name: "gunicorn from name (long arg)", + cmdline: []string{ + "gunicorn", + "--workers=2", + "-b", + "0.0.0.0", + "--name=dummy", + "test:app", + }, + expectedServiceTag: "dummy", + }, + { + name: "gunicorn from name in env", + cmdline: []string{ + "gunicorn", + "test:app", + }, + envs: []string{"GUNICORN_CMD_ARGS=--bind=127.0.0.1:8080 --workers=3 -n dummy"}, + expectedServiceTag: "dummy", + }, + { + name: "gunicorn without app found", + cmdline: []string{ + "gunicorn", + }, + envs: []string{"GUNICORN_CMD_ARGS=--bind=127.0.0.1:8080 --workers=3"}, + expectedServiceTag: "gunicorn", + }, + { + name: "gunicorn with partial wsgi app", + cmdline: []string{ + "gunicorn", + "my.package", + }, + expectedServiceTag: "my.package", + }, + { + name: "gunicorn with empty WSGI_APP env", + cmdline: []string{ + "gunicorn", + "my.package", + }, + envs: []string{"WSGI_APP="}, + expectedServiceTag: "my.package", + }, + { + name: "gunicorn with WSGI_APP env", + cmdline: []string{ + "gunicorn", + }, + envs: []string{"WSGI_APP=test:app"}, + expectedServiceTag: "test", + }, } for _, tt := range tests { From fa5ba1bb4c8fcdcee74136224e19d4ea55722e37 Mon Sep 17 00:00:00 2001 From: Tal Usvyatsky <18072279+TalUsvyatsky@users.noreply.github.com> Date: Thu, 23 May 2024 13:28:59 -0400 Subject: [PATCH 04/32] [SVLS-4850] Generate lambda enhanced CPU system, user, and total metrics (#25783) * send CPU enhanced metrics * handle windows linter * make sure metricagent ready * update integration tests snapshot and add unit test * small refactor to improve coverage * use a separate env var to gate these metrics * move timestamp into variable --- go.mod | 2 +- pkg/serverless/metrics/enhanced_metrics.go | 62 ++++++++ .../metrics/enhanced_metrics_test.go | 49 ++++++ pkg/serverless/proc/clock_unix.go | 15 ++ pkg/serverless/proc/clock_windows.go | 12 ++ pkg/serverless/proc/proc.go | 45 ++++++ pkg/serverless/proc/proc_test.go | 32 ++++ .../invalid_stat_non_numerical_value_1 | 2 + .../invalid_stat_non_numerical_value_2 | 2 + .../invalid_stat_wrong_number_columns | 2 + pkg/serverless/proc/testData/valid_stat | 10 ++ pkg/serverless/serverless.go | 11 +- .../serverless/snapshots/error-csharp | 144 ++++++++++++++++++ .../serverless/snapshots/error-java | 144 ++++++++++++++++++ .../serverless/snapshots/error-node | 144 ++++++++++++++++++ .../serverless/snapshots/error-proxy | 144 ++++++++++++++++++ .../serverless/snapshots/error-python | 144 ++++++++++++++++++ .../serverless/snapshots/metric-csharp | 144 ++++++++++++++++++ .../serverless/snapshots/metric-go | 144 ++++++++++++++++++ .../serverless/snapshots/metric-java | 144 ++++++++++++++++++ .../serverless/snapshots/metric-node | 144 ++++++++++++++++++ .../serverless/snapshots/metric-proxy | 144 ++++++++++++++++++ .../serverless/snapshots/metric-python | 144 ++++++++++++++++++ 23 files changed, 1825 insertions(+), 3 deletions(-) create mode 100644 pkg/serverless/proc/clock_unix.go create mode 100644 pkg/serverless/proc/clock_windows.go create mode 100644 pkg/serverless/proc/testData/invalid_stat_non_numerical_value_1 create mode 100644 pkg/serverless/proc/testData/invalid_stat_non_numerical_value_2 create mode 100644 pkg/serverless/proc/testData/invalid_stat_wrong_number_columns create mode 100644 pkg/serverless/proc/testData/valid_stat diff --git a/go.mod b/go.mod index 627a09aafd164d..ef4efc3c97f7d7 100644 --- a/go.mod +++ b/go.mod @@ -517,7 +517,7 @@ require ( github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/tedsuo/ifrit v0.0.0-20191009134036-9a97d0632f00 // indirect github.com/tedsuo/rata v1.0.0 // indirect - github.com/tklauser/go-sysconf v0.3.14 // indirect + github.com/tklauser/go-sysconf v0.3.14 github.com/tklauser/numcpus v0.8.0 // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/twitchtv/twirp v8.1.3+incompatible // indirect diff --git a/pkg/serverless/metrics/enhanced_metrics.go b/pkg/serverless/metrics/enhanced_metrics.go index 9fea2ab7010ee6..4be4ce04816216 100644 --- a/pkg/serverless/metrics/enhanced_metrics.go +++ b/pkg/serverless/metrics/enhanced_metrics.go @@ -14,6 +14,7 @@ import ( "github.com/DataDog/datadog-agent/pkg/aggregator" "github.com/DataDog/datadog-agent/pkg/metrics" + "github.com/DataDog/datadog-agent/pkg/serverless/proc" serverlessTags "github.com/DataDog/datadog-agent/pkg/serverless/tags" "github.com/DataDog/datadog-agent/pkg/util/log" ) @@ -44,7 +45,11 @@ const ( ErrorsMetric = "aws.lambda.enhanced.errors" invocationsMetric = "aws.lambda.enhanced.invocations" asmInvocationsMetric = "aws.lambda.enhanced.asm.invocations" + cpuSystemTimeMetric = "aws.lambda.enhanced.cpu_system_time" + cpuUserTimeMetric = "aws.lambda.enhanced.cpu_user_time" + cpuTotalTimeMetric = "aws.lambda.enhanced.cpu_total_time" enhancedMetricsEnvVar = "DD_ENHANCED_METRICS" + systemMetricsEnvVar = "DD_SYSTEM_METRICS" ) func getOutOfMemorySubstrings() []string { @@ -240,6 +245,63 @@ func SendASMInvocationEnhancedMetric(tags []string, demux aggregator.Demultiplex incrementEnhancedMetric(asmInvocationsMetric, tags, float64(time.Now().UnixNano())/float64(time.Second), demux, true) } +type GenerateCPUEnhancedMetricsArgs struct { + UserCPUTimeMs float64 + SystemCPUTimeMs float64 + Tags []string + Demux aggregator.Demultiplexer + Time time.Time +} + +// GenerateCPUEnhancedMetrics generates enhanced metrics for CPU time spent running the function in kernel mode, +// in user mode, and in total +func GenerateCPUEnhancedMetrics(args GenerateCPUEnhancedMetricsArgs) { + if strings.ToLower(os.Getenv(systemMetricsEnvVar)) == "false" { + return + } + timestamp := float64(args.Time.UnixNano()) / float64(time.Second) + args.Demux.AggregateSample(metrics.MetricSample{ + Name: cpuSystemTimeMetric, + Value: args.SystemCPUTimeMs, + Mtype: metrics.DistributionType, + Tags: args.Tags, + SampleRate: 1, + Timestamp: timestamp, + }) + args.Demux.AggregateSample(metrics.MetricSample{ + Name: cpuUserTimeMetric, + Value: args.UserCPUTimeMs, + Mtype: metrics.DistributionType, + Tags: args.Tags, + SampleRate: 1, + Timestamp: timestamp, + }) + args.Demux.AggregateSample(metrics.MetricSample{ + Name: cpuTotalTimeMetric, + Value: args.SystemCPUTimeMs + args.UserCPUTimeMs, + Mtype: metrics.DistributionType, + Tags: args.Tags, + SampleRate: 1, + Timestamp: timestamp, + }) +} + +// SendCPUEnhancedMetrics sends CPU enhanced metrics for the invocation +func SendCPUEnhancedMetrics(userCPUOffsetMs, systemCPUOffsetMs float64, tags []string, demux aggregator.Demultiplexer) { + userCPUTimeMs, systemCPUTimeMs, err := proc.GetCPUData("/proc/stat") + if err != nil { + log.Debug("Could not emit CPU enhanced metrics") + return + } + GenerateCPUEnhancedMetrics(GenerateCPUEnhancedMetricsArgs{ + UserCPUTimeMs: userCPUTimeMs - userCPUOffsetMs, + SystemCPUTimeMs: systemCPUTimeMs - systemCPUOffsetMs, + Tags: tags, + Demux: demux, + Time: time.Now(), + }) +} + // incrementEnhancedMetric sends an enhanced metric with a value of 1 to the metrics channel func incrementEnhancedMetric(name string, tags []string, timestamp float64, demux aggregator.Demultiplexer, force bool) { // TODO - pass config here, instead of directly looking up var diff --git a/pkg/serverless/metrics/enhanced_metrics_test.go b/pkg/serverless/metrics/enhanced_metrics_test.go index 8447ed14836043..6c08d5589a143f 100644 --- a/pkg/serverless/metrics/enhanced_metrics_test.go +++ b/pkg/serverless/metrics/enhanced_metrics_test.go @@ -471,6 +471,55 @@ func TestGenerateEnhancedMetricsFromRuntimeDoneLogOK(t *testing.T) { assert.Len(t, timedMetrics, 0) } +func TestGenerateCPUEnhancedMetrics(t *testing.T) { + demux := createDemultiplexer(t) + tags := []string{"functionname:test-function"} + now := time.Now() + args := GenerateCPUEnhancedMetricsArgs{100, 53, tags, demux, now} + go GenerateCPUEnhancedMetrics(args) + generatedMetrics, timedMetrics := demux.WaitForNumberOfSamples(4, 0, 100*time.Millisecond) + assert.Equal(t, generatedMetrics, []metrics.MetricSample{ + { + Name: cpuSystemTimeMetric, + Value: 53, + Mtype: metrics.DistributionType, + Tags: tags, + SampleRate: 1, + Timestamp: float64(now.UnixNano()) / float64(time.Second), + }, + { + Name: cpuUserTimeMetric, + Value: 100, + Mtype: metrics.DistributionType, + Tags: tags, + SampleRate: 1, + Timestamp: float64(now.UnixNano()) / float64(time.Second), + }, + { + Name: cpuTotalTimeMetric, + Value: 153, + Mtype: metrics.DistributionType, + Tags: tags, + SampleRate: 1, + Timestamp: float64(now.UnixNano()) / float64(time.Second), + }}) + assert.Len(t, timedMetrics, 0) +} + +func TestGenerateCPUEnhancedMetricsDisabled(t *testing.T) { + os.Setenv("DD_SYSTEM_METRICS", "false") + defer os.Setenv("DD_SYSTEM_METRICS", "true") + + demux := createDemultiplexer(t) + tags := []string{"functionname:test-function"} + now := time.Now() + args := GenerateCPUEnhancedMetricsArgs{100, 53, tags, demux, now} + go GenerateCPUEnhancedMetrics(args) + generatedMetrics, timedMetrics := demux.WaitForNumberOfSamples(4, 0, 100*time.Millisecond) + assert.Len(t, generatedMetrics, 0) + assert.Len(t, timedMetrics, 0) +} + func createDemultiplexer(t *testing.T) demultiplexer.FakeSamplerMock { return fxutil.Test[demultiplexer.FakeSamplerMock](t, logimpl.MockModule(), compressionimpl.MockModule(), demultiplexerimpl.FakeSamplerMockModule(), hostnameimpl.MockModule()) } diff --git a/pkg/serverless/proc/clock_unix.go b/pkg/serverless/proc/clock_unix.go new file mode 100644 index 00000000000000..ce6ad425f102e5 --- /dev/null +++ b/pkg/serverless/proc/clock_unix.go @@ -0,0 +1,15 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build !windows + +package proc + +import "github.com/tklauser/go-sysconf" + +func getClkTck() (int64, error) { + clcktck, err := sysconf.Sysconf(sysconf.SC_CLK_TCK) + return clcktck, err +} diff --git a/pkg/serverless/proc/clock_windows.go b/pkg/serverless/proc/clock_windows.go new file mode 100644 index 00000000000000..9006f8737adbac --- /dev/null +++ b/pkg/serverless/proc/clock_windows.go @@ -0,0 +1,12 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build windows + +package proc + +func getClkTck() (int64, error) { + return 1, nil +} diff --git a/pkg/serverless/proc/proc.go b/pkg/serverless/proc/proc.go index 98dbc584576da4..2daee65c77330a 100644 --- a/pkg/serverless/proc/proc.go +++ b/pkg/serverless/proc/proc.go @@ -7,7 +7,9 @@ package proc import ( + "bufio" "bytes" + "errors" "fmt" "os" "strconv" @@ -69,3 +71,46 @@ func SearchProcsForEnvVariable(procPath string, envName string) []string { } return result } + +// GetCPUData collects CPU usage data, returning total user CPU time, total system CPU time, error +func GetCPUData(path string) (float64, float64, error) { + file, err := os.Open(path) + if err != nil { + return 0, 0, err + } + defer file.Close() + + reader := bufio.NewReader(file) + readLine, _, err := reader.ReadLine() + if err != nil { + return 0, 0, err + } + + // The first line contains CPU totals aggregated across all CPUs + lineString := string(readLine) + cpuTotals := strings.Split(lineString, " ") + if len(cpuTotals) != 12 { + return 0, 0, errors.New("incorrect number of columns in file") + } + + // SC_CLK_TCK is the system clock frequency in ticks per second + // We'll use this to convert CPU times from user HZ to milliseconds + clcktck, err := getClkTck() + if err != nil { + return 0, 0, err + } + + userCPUTime, err := strconv.ParseFloat(cpuTotals[2], 64) + if err != nil { + return 0, 0, err + } + userCPUTimeMs := (1000 * userCPUTime) / float64(clcktck) + + systemCPUTime, err := strconv.ParseFloat(cpuTotals[4], 64) + if err != nil { + return 0, 0, err + } + systemCPUTimeMs := (1000 * systemCPUTime) / float64(clcktck) + + return userCPUTimeMs, systemCPUTimeMs, nil +} diff --git a/pkg/serverless/proc/proc_test.go b/pkg/serverless/proc/proc_test.go index 1b8f84d574f072..14f27e686313f3 100644 --- a/pkg/serverless/proc/proc_test.go +++ b/pkg/serverless/proc/proc_test.go @@ -51,3 +51,35 @@ func TestSearchProcsForEnvVariableNotFound(t *testing.T) { result := SearchProcsForEnvVariable("./testData", "xxx") assert.Equal(t, 0, len(result)) } + +func TestParseCPUTotals(t *testing.T) { + path := "./testData/valid_stat" + userCPUTimeMs, systemCPUTimeMs, err := GetCPUData(path) + assert.Equal(t, float64(23370), userCPUTimeMs) + assert.Equal(t, float64(1880), systemCPUTimeMs) + assert.Nil(t, err) + + path = "./testData/invalid_stat_non_numerical_value_1" + userCPUTimeMs, systemCPUTimeMs, err = GetCPUData(path) + assert.Equal(t, float64(0), userCPUTimeMs) + assert.Equal(t, float64(0), systemCPUTimeMs) + assert.NotNil(t, err) + + path = "./testData/invalid_stat_non_numerical_value_2" + userCPUTimeMs, systemCPUTimeMs, err = GetCPUData(path) + assert.Equal(t, float64(0), userCPUTimeMs) + assert.Equal(t, float64(0), systemCPUTimeMs) + assert.NotNil(t, err) + + path = "./testData/invalid_stat_wrong_number_columns" + userCPUTimeMs, systemCPUTimeMs, err = GetCPUData(path) + assert.Equal(t, float64(0), userCPUTimeMs) + assert.Equal(t, float64(0), systemCPUTimeMs) + assert.NotNil(t, err) + + path = "./testData/nonexistant_stat" + userCPUTimeMs, systemCPUTimeMs, err = GetCPUData(path) + assert.Equal(t, float64(0), userCPUTimeMs) + assert.Equal(t, float64(0), systemCPUTimeMs) + assert.NotNil(t, err) +} diff --git a/pkg/serverless/proc/testData/invalid_stat_non_numerical_value_1 b/pkg/serverless/proc/testData/invalid_stat_non_numerical_value_1 new file mode 100644 index 00000000000000..d72287175cca87 --- /dev/null +++ b/pkg/serverless/proc/testData/invalid_stat_non_numerical_value_1 @@ -0,0 +1,2 @@ +cpu 2337 0 INVALID 17838 8 0 16 181 0 0 +... diff --git a/pkg/serverless/proc/testData/invalid_stat_non_numerical_value_2 b/pkg/serverless/proc/testData/invalid_stat_non_numerical_value_2 new file mode 100644 index 00000000000000..816ba90096bb17 --- /dev/null +++ b/pkg/serverless/proc/testData/invalid_stat_non_numerical_value_2 @@ -0,0 +1,2 @@ +cpu INVALID 0 188 17838 8 0 16 181 0 0 +... diff --git a/pkg/serverless/proc/testData/invalid_stat_wrong_number_columns b/pkg/serverless/proc/testData/invalid_stat_wrong_number_columns new file mode 100644 index 00000000000000..7071a126daa00f --- /dev/null +++ b/pkg/serverless/proc/testData/invalid_stat_wrong_number_columns @@ -0,0 +1,2 @@ +cpu 2337 +... diff --git a/pkg/serverless/proc/testData/valid_stat b/pkg/serverless/proc/testData/valid_stat new file mode 100644 index 00000000000000..d0a0827006317c --- /dev/null +++ b/pkg/serverless/proc/testData/valid_stat @@ -0,0 +1,10 @@ +cpu 2337 0 188 17838 8 0 16 181 0 0 +cpu0 884 0 100 9188 5 0 6 95 0 0 +cpu1 1453 0 87 8649 2 0 10 85 0 0 +intr 67620 0 0 0 0 354 4356 233 1294 89 759 185 359 8 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +ctxt 135197 +btime 1716225108 +processes 1428 +procs_running 1 +procs_blocked 0 +softirq 27242 0 2425 2 696 6166 0 48 7838 0 10067 diff --git a/pkg/serverless/serverless.go b/pkg/serverless/serverless.go index 24c04e22a08ad6..8f70aa2ff0010d 100644 --- a/pkg/serverless/serverless.go +++ b/pkg/serverless/serverless.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "github.com/DataDog/datadog-agent/pkg/serverless/proc" json "github.com/json-iterator/go" "github.com/DataDog/datadog-agent/pkg/serverless/daemon" @@ -157,6 +158,7 @@ func WaitForNextInvocation(stopCh chan struct{}, daemon *daemon.Daemon, id regis } func callInvocationHandler(daemon *daemon.Daemon, arn string, deadlineMs int64, safetyBufferTimeout time.Duration, requestID string, invocationHandler InvocationHandler) { + userCPUTimeMsOffset, systemCPUTimeMsOffset, cpuOffsetErr := proc.GetCPUData("/proc/stat") timeout := computeTimeout(time.Now(), deadlineMs, safetyBufferTimeout) ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() @@ -168,9 +170,14 @@ func callInvocationHandler(daemon *daemon.Daemon, arn string, deadlineMs int64, log.Debug("Timeout detected, finishing the current invocation now to allow receiving the SHUTDOWN event") // Tell the Daemon that the runtime is done (even though it isn't, because it's timing out) so that we can receive the SHUTDOWN event daemon.TellDaemonRuntimeDone() - return + break case <-doneChannel: - return + break + } + if cpuOffsetErr == nil && daemon.MetricAgent != nil { + metrics.SendCPUEnhancedMetrics(userCPUTimeMsOffset, systemCPUTimeMsOffset, daemon.ExtraTags.Tags, daemon.MetricAgent.Demux) + } else { + log.Debug("Could not send CPU enhanced metrics") } } diff --git a/test/integration/serverless/snapshots/error-csharp b/test/integration/serverless/snapshots/error-csharp index 9b78fda5f60003..d2001da45e1e96 100644 --- a/test/integration/serverless/snapshots/error-csharp +++ b/test/integration/serverless/snapshots/error-csharp @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-csharp", + "functionname:integration-tests-extension-XXXXXX-error-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-csharp", + "functionname:integration-tests-extension-XXXXXX-error-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-csharp", + "functionname:integration-tests-extension-XXXXXX-error-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-csharp", + "functionname:integration-tests-extension-XXXXXX-error-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-csharp", + "functionname:integration-tests-extension-XXXXXX-error-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-csharp", + "functionname:integration-tests-extension-XXXXXX-error-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/error-java b/test/integration/serverless/snapshots/error-java index 1ec9bfe8073f41..d2de93a1cd4d31 100644 --- a/test/integration/serverless/snapshots/error-java +++ b/test/integration/serverless/snapshots/error-java @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-java", + "functionname:integration-tests-extension-XXXXXX-error-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-java", + "functionname:integration-tests-extension-XXXXXX-error-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-java", + "functionname:integration-tests-extension-XXXXXX-error-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-java", + "functionname:integration-tests-extension-XXXXXX-error-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-java", + "functionname:integration-tests-extension-XXXXXX-error-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-java", + "functionname:integration-tests-extension-XXXXXX-error-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/error-node b/test/integration/serverless/snapshots/error-node index 61f10f24b75161..cd6a3e3a45db98 100644 --- a/test/integration/serverless/snapshots/error-node +++ b/test/integration/serverless/snapshots/error-node @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-node", + "functionname:integration-tests-extension-XXXXXX-error-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-node", + "functionname:integration-tests-extension-XXXXXX-error-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-node", + "functionname:integration-tests-extension-XXXXXX-error-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-node", + "functionname:integration-tests-extension-XXXXXX-error-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-node", + "functionname:integration-tests-extension-XXXXXX-error-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-node", + "functionname:integration-tests-extension-XXXXXX-error-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/error-proxy b/test/integration/serverless/snapshots/error-proxy index 3cf9e94d49e45d..c062ff2308f915 100644 --- a/test/integration/serverless/snapshots/error-proxy +++ b/test/integration/serverless/snapshots/error-proxy @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-proxy", + "functionname:integration-tests-extension-XXXXXX-error-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-proxy", + "functionname:integration-tests-extension-XXXXXX-error-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-proxy", + "functionname:integration-tests-extension-XXXXXX-error-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-proxy", + "functionname:integration-tests-extension-XXXXXX-error-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-proxy", + "functionname:integration-tests-extension-XXXXXX-error-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-proxy", + "functionname:integration-tests-extension-XXXXXX-error-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/error-python b/test/integration/serverless/snapshots/error-python index bae49737f5b286..e320e73f7bb686 100644 --- a/test/integration/serverless/snapshots/error-python +++ b/test/integration/serverless/snapshots/error-python @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-python", + "functionname:integration-tests-extension-XXXXXX-error-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-python", + "functionname:integration-tests-extension-XXXXXX-error-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-python", + "functionname:integration-tests-extension-XXXXXX-error-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-python", + "functionname:integration-tests-extension-XXXXXX-error-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-python", + "functionname:integration-tests-extension-XXXXXX-error-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-error-python", + "functionname:integration-tests-extension-XXXXXX-error-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-error-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/metric-csharp b/test/integration/serverless/snapshots/metric-csharp index 0ebd3c754e43ea..f8d73609095ea5 100644 --- a/test/integration/serverless/snapshots/metric-csharp +++ b/test/integration/serverless/snapshots/metric-csharp @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-csharp", + "functionname:integration-tests-extension-XXXXXX-metric-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-csharp", + "functionname:integration-tests-extension-XXXXXX-metric-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-csharp", + "functionname:integration-tests-extension-XXXXXX-metric-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-csharp", + "functionname:integration-tests-extension-XXXXXX-metric-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-csharp", + "functionname:integration-tests-extension-XXXXXX-metric-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-csharp", + "functionname:integration-tests-extension-XXXXXX-metric-csharp", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-csharp", + "runtime:dotnet6", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/metric-go b/test/integration/serverless/snapshots/metric-go index 17159b43de8142..3a7a1da81843bb 100644 --- a/test/integration/serverless/snapshots/metric-go +++ b/test/integration/serverless/snapshots/metric-go @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-go", + "functionname:integration-tests-extension-XXXXXX-metric-go", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-go", + "runtime:provided.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-go", + "functionname:integration-tests-extension-XXXXXX-metric-go", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-go", + "runtime:provided.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-go", + "functionname:integration-tests-extension-XXXXXX-metric-go", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-go", + "runtime:provided.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-go", + "functionname:integration-tests-extension-XXXXXX-metric-go", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-go", + "runtime:provided.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-go", + "functionname:integration-tests-extension-XXXXXX-metric-go", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-go", + "runtime:provided.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-go", + "functionname:integration-tests-extension-XXXXXX-metric-go", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-go", + "runtime:provided.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/metric-java b/test/integration/serverless/snapshots/metric-java index d6300406219862..ba71ca142c2cbb 100644 --- a/test/integration/serverless/snapshots/metric-java +++ b/test/integration/serverless/snapshots/metric-java @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-java", + "functionname:integration-tests-extension-XXXXXX-metric-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-java", + "functionname:integration-tests-extension-XXXXXX-metric-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-java", + "functionname:integration-tests-extension-XXXXXX-metric-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-java", + "functionname:integration-tests-extension-XXXXXX-metric-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-java", + "functionname:integration-tests-extension-XXXXXX-metric-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-java", + "functionname:integration-tests-extension-XXXXXX-metric-java", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-java", + "runtime:java8.al2", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/metric-node b/test/integration/serverless/snapshots/metric-node index a4895091ab19f9..064b88aeec8191 100644 --- a/test/integration/serverless/snapshots/metric-node +++ b/test/integration/serverless/snapshots/metric-node @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-node", + "functionname:integration-tests-extension-XXXXXX-metric-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-node", + "functionname:integration-tests-extension-XXXXXX-metric-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-node", + "functionname:integration-tests-extension-XXXXXX-metric-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-node", + "functionname:integration-tests-extension-XXXXXX-metric-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-node", + "functionname:integration-tests-extension-XXXXXX-metric-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-node", + "functionname:integration-tests-extension-XXXXXX-metric-node", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-node", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/metric-proxy b/test/integration/serverless/snapshots/metric-proxy index 85e42e46cd9cd4..6e3ac585df3e46 100644 --- a/test/integration/serverless/snapshots/metric-proxy +++ b/test/integration/serverless/snapshots/metric-proxy @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-proxy", + "functionname:integration-tests-extension-XXXXXX-metric-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-proxy", + "functionname:integration-tests-extension-XXXXXX-metric-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-proxy", + "functionname:integration-tests-extension-XXXXXX-metric-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-proxy", + "functionname:integration-tests-extension-XXXXXX-metric-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-proxy", + "functionname:integration-tests-extension-XXXXXX-metric-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-proxy", + "functionname:integration-tests-extension-XXXXXX-metric-proxy", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-proxy", + "runtime:nodejs18.x", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], diff --git a/test/integration/serverless/snapshots/metric-python b/test/integration/serverless/snapshots/metric-python index 8b39a82c880ab0..1458a03434fdde 100644 --- a/test/integration/serverless/snapshots/metric-python +++ b/test/integration/serverless/snapshots/metric-python @@ -49,6 +49,150 @@ "version:integration-tests-version" ] }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-python", + "functionname:integration-tests-extension-XXXXXX-metric-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_system_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-python", + "functionname:integration-tests-extension-XXXXXX-metric-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-python", + "functionname:integration-tests-extension-XXXXXX-metric-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_total_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-python", + "functionname:integration-tests-extension-XXXXXX-metric-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-python", + "functionname:integration-tests-extension-XXXXXX-metric-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, + { + "distributions": null, + "dogsketches": [], + "metric": "aws.lambda.enhanced.cpu_user_time", + "tags": [ + "account_id:############", + "architecture:XXX", + "aws_account:############", + "dd_extension_version:123", + "env:integration-tests-env", + "function_arn:arn:aws:lambda:eu-west-1:############:function:integration-tests-extension-XXXXXX-metric-python", + "functionname:integration-tests-extension-XXXXXX-metric-python", + "memorysize:1024", + "region:eu-west-1", + "resource:integration-tests-extension-XXXXXX-metric-python", + "runtime:python3.8", + "service:integration-tests-service", + "taga:valuea", + "tagb:valueb", + "tagc:valuec", + "tagd:valued", + "version:integration-tests-version" + ] + }, { "distributions": null, "dogsketches": [], From ece035dccfbae4701ca83b3ebe42807825005edd Mon Sep 17 00:00:00 2001 From: Alexandre Yang Date: Thu, 23 May 2024 22:12:48 +0200 Subject: [PATCH 05/32] [network-path] Add protocol (#25867) * [network-path] Add protocol * fix test --- .../npcollector/npcollectorimpl/npcollector_test.go | 4 ++++ pkg/networkpath/payload/payload.go | 11 +++++++++++ pkg/networkpath/traceroute/runner.go | 1 + 3 files changed, 16 insertions(+) diff --git a/comp/networkpath/npcollector/npcollectorimpl/npcollector_test.go b/comp/networkpath/npcollector/npcollectorimpl/npcollector_test.go index 26f23cebc248f2..6457dddad1393a 100644 --- a/comp/networkpath/npcollector/npcollectorimpl/npcollector_test.go +++ b/comp/networkpath/npcollector/npcollectorimpl/npcollector_test.go @@ -96,6 +96,7 @@ func Test_NpCollector_runningAndProcessing(t *testing.T) { var p payload.NetworkPath if cfg.DestHostname == "127.0.0.2" { p = payload.NetworkPath{ + Protocol: payload.ProtocolUDP, Source: payload.NetworkPathSource{Hostname: "abc"}, Destination: payload.NetworkPathDestination{Hostname: "abc", IPAddress: "127.0.0.2", Port: 80}, Hops: []payload.NetworkPathHop{ @@ -106,6 +107,7 @@ func Test_NpCollector_runningAndProcessing(t *testing.T) { } if cfg.DestHostname == "127.0.0.4" { p = payload.NetworkPath{ + Protocol: payload.ProtocolUDP, Source: payload.NetworkPathSource{Hostname: "abc"}, Destination: payload.NetworkPathDestination{Hostname: "abc", IPAddress: "127.0.0.4", Port: 80}, Hops: []payload.NetworkPathHop{ @@ -124,6 +126,7 @@ func Test_NpCollector_runningAndProcessing(t *testing.T) { "timestamp": 0, "namespace": "", "path_id": "", + "protocol": "UDP", "source": { "hostname": "abc", "via": null, @@ -159,6 +162,7 @@ func Test_NpCollector_runningAndProcessing(t *testing.T) { "timestamp": 0, "namespace": "", "path_id": "", + "protocol": "UDP", "source": { "hostname": "abc", "via": null, diff --git a/pkg/networkpath/payload/payload.go b/pkg/networkpath/payload/payload.go index 66b1a0517f375d..0af410c48ebb40 100644 --- a/pkg/networkpath/payload/payload.go +++ b/pkg/networkpath/payload/payload.go @@ -8,6 +8,16 @@ package payload import "github.com/DataDog/datadog-agent/pkg/network" +// Protocol defines supported network protocols +type Protocol string + +const ( + // ProtocolTCP is the TCP protocol. + ProtocolTCP Protocol = "TCP" + // ProtocolUDP is the UDP protocol. + ProtocolUDP Protocol = "UDP" +) + // NetworkPathHop encapsulates the data for a single // hop within a path type NetworkPathHop struct { @@ -40,6 +50,7 @@ type NetworkPath struct { Timestamp int64 `json:"timestamp"` Namespace string `json:"namespace"` // namespace used to resolve NDM resources PathID string `json:"path_id"` + Protocol Protocol `json:"protocol"` Source NetworkPathSource `json:"source"` Destination NetworkPathDestination `json:"destination"` Hops []NetworkPathHop `json:"hops"` diff --git a/pkg/networkpath/traceroute/runner.go b/pkg/networkpath/traceroute/runner.go index 157352fb9108f7..460ee4eb2de77d 100644 --- a/pkg/networkpath/traceroute/runner.go +++ b/pkg/networkpath/traceroute/runner.go @@ -183,6 +183,7 @@ func (r *Runner) processResults(res *results.Results, hname string, destinationH traceroutePath := payload.NetworkPath{ PathID: pathID, + Protocol: payload.ProtocolUDP, Timestamp: time.Now().UnixMilli(), Source: payload.NetworkPathSource{ Hostname: hname, From 33d43600c674a65c10336b0edadc2b677c6604a0 Mon Sep 17 00:00:00 2001 From: Derek Brown Date: Thu, 23 May 2024 15:07:19 -0700 Subject: [PATCH 06/32] =?UTF-8?q?[windows][cws]wkint-491]=20Add=20helper?= =?UTF-8?q?=20functions=20for=20normalizing=20drive=20p=E2=80=A6=20(#25872?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [windows][cws]wkint-491] Add helper functions for normalizing drive paths. Creates a map of device names to drive paths; is intended for consumption by future pr. * add unused to convertDrivePath() because it will be consumed in a future PR * fix oversight in test initialization --- .../probe/probe_kernel_file_windows.go | 70 +++++++++++++++++++ .../probe/probe_kernel_file_windows_test.go | 2 + pkg/security/probe/probe_windows.go | 6 ++ 3 files changed, 78 insertions(+) diff --git a/pkg/security/probe/probe_kernel_file_windows.go b/pkg/security/probe/probe_kernel_file_windows.go index 0e52a056563b44..b11f46a7334356 100644 --- a/pkg/security/probe/probe_kernel_file_windows.go +++ b/pkg/security/probe/probe_kernel_file_windows.go @@ -15,6 +15,9 @@ import ( "github.com/DataDog/datadog-agent/comp/etw" etwimpl "github.com/DataDog/datadog-agent/comp/etw/impl" + "github.com/DataDog/datadog-agent/pkg/util/winutil" + + "golang.org/x/sys/windows" ) const ( @@ -697,3 +700,70 @@ func (wp *WindowsProbe) parseNameDeleteArgs(e *etw.DDEventRecord) (*nameDeleteAr } return (*nameDeleteArgs)(ca), nil } + +// nolint: unused +func (wp *WindowsProbe) convertDrivePath(devicefilename string) (string, error) { + // filepath doesn't seem to like the \Device\HarddiskVolume1 format + pathchunks := strings.Split(devicefilename, "\\") + if len(pathchunks) > 2 { + if strings.EqualFold(pathchunks[1], "device") { + pathchunks[2] = wp.volumeMap[strings.ToLower(pathchunks[2])] + return filepath.Join(pathchunks[2:]...), nil + } + } + return "", fmt.Errorf("Unable to parse path %v", devicefilename) +} +func (wp *WindowsProbe) initializeVolumeMap() error { + + buf := make([]uint16, 1024) + bufferLength := uint32(len(buf)) + + _, err := windows.GetLogicalDriveStrings(bufferLength, &buf[0]) + if err != nil { + return err + } + drives := winutil.ConvertWindowsStringList(buf) + for _, drive := range drives { + t := windows.GetDriveType(windows.StringToUTF16Ptr(drive[:3])) + /* + DRIVE_UNKNOWN + 0 + The drive type cannot be determined. + DRIVE_NO_ROOT_DIR + 1 + The root path is invalid; for example, there is no volume mounted at the specified path. + DRIVE_REMOVABLE + 2 + The drive has removable media; for example, a floppy drive, thumb drive, or flash card reader. + DRIVE_FIXED + 3 + The drive has fixed media; for example, a hard disk drive or flash drive. + DRIVE_REMOTE + 4 + The drive is a remote (network) drive. + DRIVE_CDROM + 5 + The drive is a CD-ROM drive. + DRIVE_RAMDISK + 6 + The drive is a RAM disk. + */ + if t == windows.DRIVE_FIXED { + volpath := make([]uint16, 1024) + vollen := uint32(len(volpath)) + _, err = windows.QueryDosDevice(windows.StringToUTF16Ptr(drive[:2]), &volpath[0], vollen) + if err == nil { + devname := windows.UTF16PtrToString(&volpath[0]) + paths := strings.Split(devname, "\\") // apparently, filepath.split doesn't like volume names + + if len(paths) > 2 { + // the \Device leads to the first entry being empty + if strings.EqualFold(paths[1], "device") { + wp.volumeMap[strings.ToLower(paths[2])] = drive + } + } + } + } + } + return nil +} diff --git a/pkg/security/probe/probe_kernel_file_windows_test.go b/pkg/security/probe/probe_kernel_file_windows_test.go index 21a210bc2f1862..b25db296ad6b94 100644 --- a/pkg/security/probe/probe_kernel_file_windows_test.go +++ b/pkg/security/probe/probe_kernel_file_windows_test.go @@ -68,6 +68,8 @@ func createTestProbe() (*WindowsProbe, error) { isRenameEnabled: true, isWriteEnabled: true, isDeleteEnabled: true, + + volumeMap: make(map[string]string), } err = wp.Init() diff --git a/pkg/security/probe/probe_windows.go b/pkg/security/probe/probe_windows.go index 5326ca3d288c4c..5e6501f31e8144 100644 --- a/pkg/security/probe/probe_windows.go +++ b/pkg/security/probe/probe_windows.go @@ -84,6 +84,9 @@ type WindowsProbe struct { discardedPaths *lru.Cache[string, struct{}] discardedBasenames *lru.Cache[string, struct{}] + // map of device path to volume name (i.e. c:) + volumeMap map[string]string + // actions processKiller *ProcessKiller @@ -171,6 +174,7 @@ func (p *WindowsProbe) initEtwFIM() error { if !p.config.RuntimeSecurity.FIMEnabled { return nil } + _ = p.initializeVolumeMap() // log at Warning right now because it's not expected to be enabled log.Warnf("Enabling FIM processing") etwSessionName := "SystemProbeFIM_ETW" @@ -893,6 +897,8 @@ func NewWindowsProbe(probe *Probe, config *config.Config, opts Opts) (*WindowsPr discardedPaths: discardedPaths, discardedBasenames: discardedBasenames, + volumeMap: make(map[string]string), + processKiller: NewProcessKiller(), } From 7b020ae0dd50d2c68d937ecee79649fc378d239b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 May 2024 07:40:26 +0000 Subject: [PATCH 07/32] CWS: sync BTFhub constants (#25877) Co-authored-by: --- pkg/security/probe/constantfetch/btfhub/constants.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/security/probe/constantfetch/btfhub/constants.json b/pkg/security/probe/constantfetch/btfhub/constants.json index 6c63ee53bc594d..5ec7174d8dc535 100644 --- a/pkg/security/probe/constantfetch/btfhub/constants.json +++ b/pkg/security/probe/constantfetch/btfhub/constants.json @@ -23300,6 +23300,13 @@ "uname_release": "4.14.35-2047.537.1.el7uek.x86_64", "cindex": 96 }, + { + "distrib": "ol", + "version": "7", + "arch": "x86_64", + "uname_release": "4.14.35-2047.537.3.el7uek.x86_64", + "cindex": 96 + }, { "distrib": "ol", "version": "7", From b3df4674914a0ed315c1c459a88bdc270c8c4ec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lian=20Raimbault?= <161456554+CelianR@users.noreply.github.com> Date: Fri, 24 May 2024 10:40:10 +0200 Subject: [PATCH 08/32] wip (#24817) wip [e2e-change-path] Added yaml parsing [e2e-change-path] Added test files [e2e-change-path] Cleaned code, updated read includes [e2e-change-path] Added clean summary [e2e-change-path] Added ok message [e2e-change-path] Verified exactly for changes.paths [e2e-change-path] Removed type hinting to pass windows linting [e2e-change-path] Added custom lru cache [e2e-change-path] Removed .data [e2e-change-path] Fixed reference not working anymore [e2e-change-path] Refactored + test/* doesnt count for change path Update tasks/libs/ciproviders/gitlab_api.py [e2e-change-path] Added comments [e2e-change-path] Windows complient [e2e-change-path] Applied suggestions Update tasks/linter.py Co-authored-by: Kevin Fairise <132568982+KevinFairise2@users.noreply.github.com> --- tasks/libs/ciproviders/gitlab_api.py | 222 ++++++++++++++++-- tasks/linter.py | 46 ++++ tasks/unit-tests/gitlab_api_tests.py | 16 ++ tasks/unit-tests/testdata/yaml_extends.yml | 19 ++ .../testdata/yaml_extends_reference.yml | 37 +++ tasks/unit-tests/testdata/yaml_reference.yml | 28 +++ 6 files changed, 348 insertions(+), 20 deletions(-) create mode 100644 tasks/unit-tests/testdata/yaml_extends.yml create mode 100644 tasks/unit-tests/testdata/yaml_extends_reference.yml create mode 100644 tasks/unit-tests/testdata/yaml_reference.yml diff --git a/tasks/libs/ciproviders/gitlab_api.py b/tasks/libs/ciproviders/gitlab_api.py index 9220e35e4b8168..3583849a0298ba 100644 --- a/tasks/libs/ciproviders/gitlab_api.py +++ b/tasks/libs/ciproviders/gitlab_api.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import json import os import platform import subprocess -from collections import UserList +from functools import lru_cache import gitlab import yaml @@ -82,6 +84,34 @@ def refresh_pipeline(pipeline: ProjectPipeline): pipeline.refresh() +class ConfigNodeList(list): + """ + Wrapper of list to allow hashing and lru cache + """ + + def __init__(self, *args, **kwargs) -> None: + self.extend(*args, **kwargs) + + def __hash__(self) -> int: + return id(self) + + +class YamlReferenceTagList(ConfigNodeList): + pass + + +class ConfigNodeDict(dict): + """ + Wrapper of dict to allow hashing and lru cache + """ + + def __init__(self, *args, **kwargs) -> None: + self.update(*args, **kwargs) + + def __hash__(self) -> int: + return id(self) + + class ReferenceTag(yaml.YAMLObject): """ Custom yaml tag to handle references in gitlab-ci configuration @@ -94,25 +124,150 @@ def __init__(self, references): @classmethod def from_yaml(cls, loader, node): - return UserList(loader.construct_sequence(node)) + return YamlReferenceTagList(loader.construct_sequence(node)) @classmethod def to_yaml(cls, dumper, data): - return dumper.represent_sequence(cls.yaml_tag, data.data, flow_style=True) + return dumper.represent_sequence(cls.yaml_tag, data, flow_style=True) + + +def convert_to_config_node(json_data): + """ + Convert json data to ConfigNode + """ + if isinstance(json_data, dict): + return ConfigNodeDict({k: convert_to_config_node(v) for k, v in json_data.items()}) + elif isinstance(json_data, list): + constructor = YamlReferenceTagList if isinstance(json_data, YamlReferenceTagList) else ConfigNodeList + + return constructor([convert_to_config_node(v) for v in json_data]) + else: + return json_data + + +def apply_yaml_extends(config: dict, node): + """ + Applies `extends` yaml tags to the node and its children inplace + + > Example: + Config: + ```yaml + .parent: + hello: world + node: + extends: .parent + ``` + + apply_yaml_extends(node) updates node to: + ```yaml + node: + hello: world + ``` + """ + # Ensure node is an object that can contain extends + if not isinstance(node, dict): + return + + if 'extends' in node: + parents = node['extends'] + if isinstance(parents, str): + parents = [parents] + + # Merge parent + for parent_name in parents: + parent = config[parent_name] + apply_yaml_postprocessing(config, parent) + for key, value in parent.items(): + if key not in node: + node[key] = value + + del node['extends'] + + +def apply_yaml_reference(config: dict, node): + """ + Applies `!reference` gitlab yaml tags to the node and its children inplace + + > Example: + Config: + ```yaml + .colors: + - red + - green + - blue + node: + colors: !reference [.colors] + ``` + + apply_yaml_extends(node) updates node to: + ```yaml + node: + colors: + - red + - green + - blue + ``` + """ + + def apply_ref(value): + """ + Applies reference tags + """ + if isinstance(value, YamlReferenceTagList): + assert value != [], 'Empty reference tag' + + # !reference [a, b, c] means we are looking for config[a][b][c] + ref_value = config[value[0]] + for i in range(1, len(value)): + ref_value = ref_value[value[i]] + + apply_yaml_postprocessing(config, ref_value) + return ref_value + else: + apply_yaml_postprocessing(config, value) -def generate_gitlab_full_configuration(input_file, context=None, compare_to=None): + return value + + if isinstance(node, dict): + for key, value in node.items(): + node[key] = apply_ref(value) + elif isinstance(node, list): + for i, value in enumerate(node): + node[i] = apply_ref(value) + + +@lru_cache(maxsize=None) +def apply_yaml_postprocessing(config: ConfigNodeDict, node): + if isinstance(node, dict): + for value in node.values(): + apply_yaml_postprocessing(config, value) + elif isinstance(node, list): + for value in node: + apply_yaml_postprocessing(config, value) + + apply_yaml_extends(config, node) + apply_yaml_reference(config, node) + + +def generate_gitlab_full_configuration( + input_file, context=None, compare_to=None, return_dump=True, apply_postprocessing=False +): """ Generate a full gitlab-ci configuration by resolving all includes + + - input_file: Initial gitlab yaml file (.gitlab-ci.yml) + - context: Gitlab variables + - compare_to: Override compare_to on change rules + - return_dump: Whether to return the string dump or the dict object representing the configuration + - apply_postprocessing: Whether or not to solve `extends` and `!reference` tags """ # Update loader/dumper to handle !reference tag yaml.SafeLoader.add_constructor(ReferenceTag.yaml_tag, ReferenceTag.from_yaml) - yaml.SafeDumper.add_representer(UserList, ReferenceTag.to_yaml) - yaml_contents = [] - read_includes(input_file, yaml_contents) - full_configuration = {} - for yaml_file in yaml_contents: - full_configuration.update(yaml_file) + yaml.SafeDumper.add_representer(YamlReferenceTagList, ReferenceTag.to_yaml) + + full_configuration = read_includes(input_file, return_config=True) + # Override some variables with a dedicated context if context: full_configuration["variables"].update(context) @@ -134,21 +289,48 @@ def generate_gitlab_full_configuration(input_file, context=None, compare_to=None and "compare_to" in v["changes"] ): v["changes"]["compare_to"] = compare_to - return yaml.safe_dump(full_configuration) + if apply_postprocessing: + # We have to use ConfigNode to allow hashing and lru cache + full_configuration = convert_to_config_node(full_configuration) + apply_yaml_postprocessing(full_configuration, full_configuration) -def read_includes(yaml_file, includes): + return yaml.safe_dump(full_configuration) if return_dump else full_configuration + + +def read_includes(yaml_files, includes=None, return_config=False, add_file_path=False): """ Recursive method to read all includes from yaml files and store them in a list + - add_file_path: add the file path to each object of the parsed file """ - current_file = read_content(yaml_file) - if 'include' not in current_file: - includes.append(current_file) - else: - for include in current_file['include']: - read_includes(include, includes) - del current_file['include'] - includes.append(current_file) + if includes is None: + includes = [] + + if isinstance(yaml_files, str): + yaml_files = [yaml_files] + + for yaml_file in yaml_files: + current_file = read_content(yaml_file) + + if add_file_path: + for value in current_file.values(): + if isinstance(value, dict): + value['_file_path'] = yaml_file + + if 'include' not in current_file: + includes.append(current_file) + else: + read_includes(current_file['include'], includes, add_file_path=add_file_path) + del current_file['include'] + includes.append(current_file) + + # Merge all files + if return_config: + full_configuration = {} + for yaml_file in includes: + full_configuration.update(yaml_file) + + return full_configuration def read_content(file_path): diff --git a/tasks/linter.py b/tasks/linter.py index 9683b6a782ece5..238de45c750c96 100644 --- a/tasks/linter.py +++ b/tasks/linter.py @@ -4,6 +4,7 @@ import re import sys from collections import defaultdict +from glob import glob from invoke import Exit, task @@ -17,6 +18,7 @@ get_gitlab_repo, get_preset_contexts, load_context, + read_includes, ) from tasks.libs.common.check_tools_version import check_tools_version from tasks.libs.common.utils import DEFAULT_BRANCH, GITHUB_REPO_NAME, color_message, is_pr_context, running_in_ci @@ -385,3 +387,47 @@ def releasenote(ctx): def update_go(_): _update_references(warn=False, version="1.2.3", dry_run=True) _update_go_mods(warn=False, version="1.2.3", include_otel_modules=True, dry_run=True) + + +@task(iterable=['job_files']) +def test_change_path(_, job_files=None): + """ + Verify that the jobs defined within job_files contain a change path rule. + """ + job_files = job_files or (['.gitlab/e2e/e2e.yml'] + list(glob('.gitlab/kitchen_testing/new-e2e_testing/*.yml'))) + + # Read gitlab config + config = generate_gitlab_full_configuration(".gitlab-ci.yml", {}, return_dump=False, apply_postprocessing=True) + + # Fetch all test jobs + test_config = read_includes(job_files, return_config=True, add_file_path=True) + tests = [(test, data['_file_path']) for test, data in test_config.items() if test[0] != '.'] + + def contains_valid_change_rule(rule): + """ + Verifies that the job rule contains the required change path configuration. + """ + if 'changes' not in rule or 'paths' not in rule['changes']: + return False + + # The change paths should be more than just test files + return any( + not path.startswith(('test/', './test/', 'test\\', '.\\test\\')) for path in rule['changes']['paths'] + ) + + # Verify that all tests contain a change path rule + tests_without_change_path = defaultdict(list) + for test, filepath in tests: + if not any(contains_valid_change_rule(rule) for rule in config[test]['rules'] if isinstance(rule, dict)): + tests_without_change_path[filepath].append(test) + + if len(tests_without_change_path) != 0: + print(color_message("error: Tests without required change paths rule:", "red"), file=sys.stderr) + for filepath, tests in tests_without_change_path.items(): + print(f"- {color_message(filepath, 'bold')}: {', '.join(tests)}", file=sys.stderr) + + raise RuntimeError( + 'Some tests do not contain required change paths rule, they must contain at least one non-test path.' + ) + else: + print(color_message("success: All tests contain a change paths rule", "green")) diff --git a/tasks/unit-tests/gitlab_api_tests.py b/tasks/unit-tests/gitlab_api_tests.py index 24399f816c8bdf..6052c9ba66eb7b 100644 --- a/tasks/unit-tests/gitlab_api_tests.py +++ b/tasks/unit-tests/gitlab_api_tests.py @@ -29,3 +29,19 @@ def test_yaml_with_reference(self): with open("tasks/unit-tests/testdata/expected.yml") as f: expected = f.read() self.assertEqual(full_configuration, expected) + + +class TestGitlabYaml(unittest.TestCase): + def make_test(self, file): + config = generate_gitlab_full_configuration(file, return_dump=False, apply_postprocessing=True) + + self.assertDictEqual(config['target'], config['expected']) + + def test_reference(self): + self.make_test("tasks/unit-tests/testdata/yaml_reference.yml") + + def test_extends(self): + self.make_test("tasks/unit-tests/testdata/yaml_extends.yml") + + def test_extends_reference(self): + self.make_test("tasks/unit-tests/testdata/yaml_extends_reference.yml") diff --git a/tasks/unit-tests/testdata/yaml_extends.yml b/tasks/unit-tests/testdata/yaml_extends.yml new file mode 100644 index 00000000000000..6a766884636f1f --- /dev/null +++ b/tasks/unit-tests/testdata/yaml_extends.yml @@ -0,0 +1,19 @@ +grand-parent: + attribute: hello world + +father: + extends: grand-parent + some-attribute: 42 + +mother: + say-hi: "Hi !" + +target: + extends: [father, mother] + say-hello: "Hello :wave:" + +expected: + attribute: hello world + some-attribute: 42 + say-hi: "Hi !" + say-hello: "Hello :wave:" diff --git a/tasks/unit-tests/testdata/yaml_extends_reference.yml b/tasks/unit-tests/testdata/yaml_extends_reference.yml new file mode 100644 index 00000000000000..cdab17d1f2bafd --- /dev/null +++ b/tasks/unit-tests/testdata/yaml_extends_reference.yml @@ -0,0 +1,37 @@ +.name: Joseph + +.a: + name: !reference [.name] + +.all_colors: + colors: + - red + - green + - blue + - yellow + - magenta + - cyan + +.colors: + extends: .all_colors + +.b: + colors: !reference [.colors] + +.name2: Alice + +target: + extends: + - .a + - .b + names: + - !reference [.name] + - !reference [.name2] + +expected: + name: Joseph + names: + - Joseph + - Alice + colors: + colors: [red, green, blue, yellow, magenta, cyan] diff --git a/tasks/unit-tests/testdata/yaml_reference.yml b/tasks/unit-tests/testdata/yaml_reference.yml new file mode 100644 index 00000000000000..7089b14ead41f0 --- /dev/null +++ b/tasks/unit-tests/testdata/yaml_reference.yml @@ -0,0 +1,28 @@ +.colors: + - red + - green + - blue + +.extended: + colors: + - yellow + - magenta + - cyan + +.name: Joseph + +target: + name: !reference [.name] + colors: !reference [.colors] + extended_colors: !reference [.extended, colors] + +expected: + name: Joseph + colors: + - red + - green + - blue + extended_colors: + - yellow + - magenta + - cyan From 2f5a7b92731c170b0a4c94c94d6233a1c3f4a3b4 Mon Sep 17 00:00:00 2001 From: Guillaume Fournier <36961134+Gui774ume@users.noreply.github.com> Date: Fri, 24 May 2024 11:26:52 +0200 Subject: [PATCH 09/32] [CSM] `imds` event type (#25317) * [CSM] Capture IMDS traffic * [CSM] Add AWS Security Credentials in process context * [CSM] Fix missing entries in traced_pids and reset DNS event * [CSM] Add IMDS events in Activity Dumps and generate drift events for them * [CSM] release note * [CSM] Add support for IBM and Oracle * [CSM] Make IMDS IP configurable * [CSM] Select network probes for IMDS event type * [CSM] Add IMDS nodes to the Activity Tree debug call * [CSM] Add IMDS tests * [CSM] Fix documentation * [CSM] Fix dump load controller test * [CSM] Rebase main * [CSM] Disable Network ingress TC probes by default --- .../agent_expressions.md | 72 ++ docs/cloud-workload-security/backend.md | 269 +++++++ .../backend.schema.json | 108 +++ docs/cloud-workload-security/secl.json | 145 ++++ pkg/config/setup/system_probe.go | 1 + pkg/config/setup/system_probe_cws.go | 7 +- pkg/security/config/config.go | 25 +- .../ebpf/c/include/constants/custom.h | 17 +- pkg/security/ebpf/c/include/constants/enums.h | 1 + .../ebpf/c/include/events_definition.h | 10 + pkg/security/ebpf/c/include/helpers/all.h | 1 + pkg/security/ebpf/c/include/helpers/dns.h | 1 + pkg/security/ebpf/c/include/helpers/imds.h | 50 ++ pkg/security/ebpf/c/include/hooks/all.h | 1 + .../ebpf/c/include/hooks/network/imds.h | 36 + .../ebpf/c/include/hooks/network/router.h | 8 +- .../ebpf/c/include/hooks/network/tc.h | 20 +- pkg/security/ebpf/c/include/hooks/procfs.h | 3 + pkg/security/ebpf/c/include/maps.h | 1 + pkg/security/ebpf/probes/all.go | 2 +- pkg/security/ebpf/probes/const.go | 6 +- pkg/security/ebpf/probes/event_types.go | 9 + pkg/security/ebpf/probes/tc.go | 31 +- pkg/security/probe/config/config.go | 3 + pkg/security/probe/field_handlers_ebpf.go | 5 + pkg/security/probe/field_handlers_ebpfless.go | 5 + pkg/security/probe/probe_ebpf.go | 12 +- .../resolvers/process/resolver_ebpf.go | 47 ++ pkg/security/resolvers/tc/resolver.go | 2 +- pkg/security/secl/model/accessors_unix.go | 185 +++++ pkg/security/secl/model/category.go | 2 +- pkg/security/secl/model/consts_common.go | 17 + pkg/security/secl/model/events.go | 4 + .../secl/model/field_accessors_unix.go | 64 ++ .../secl/model/field_handlers_unix.go | 1 + pkg/security/secl/model/model.go | 30 + pkg/security/secl/model/model_helpers_unix.go | 6 + pkg/security/secl/model/model_unix.go | 3 + .../secl/model/unmarshallers_linux.go | 93 +++ .../activity_tree/activity_tree.go | 11 + .../activity_tree/activity_tree_graph.go | 55 +- .../activity_tree_proto_dec_v1.go | 68 ++ .../activity_tree_proto_enc_v1.go | 47 ++ .../activity_tree/activity_tree_stats.go | 2 + .../activity_tree/imds_node.go | 50 ++ .../activity_tree/process_node.go | 37 +- .../security_profile/dump/load_controller.go | 2 +- pkg/security/security_profile/dump/manager.go | 8 +- pkg/security/serializers/serializers_base.go | 78 ++ .../serializers_base_linux_easyjson.go | 404 ++++++++-- pkg/security/serializers/serializers_linux.go | 13 + .../serializers/serializers_linux_easyjson.go | 256 +++--- pkg/security/tests/activity_dumps_common.go | 2 +- .../activity_dumps_loadcontroller_test.go | 33 +- pkg/security/tests/activity_dumps_test.go | 55 +- pkg/security/tests/cmdwrapper.go | 2 +- pkg/security/tests/filters_test.go | 10 +- pkg/security/tests/imds_test.go | 737 ++++++++++++++++++ pkg/security/tests/imds_utils/imds_utils.go | 138 ++++ pkg/security/tests/module_tester.go | 1 + pkg/security/tests/module_tester_linux.go | 56 +- pkg/security/tests/network_device_test.go | 4 +- pkg/security/tests/schemas.go | 6 + pkg/security/tests/schemas/imds.schema.json | 93 +++ .../syscall_tester/go/syscall_go_tester.go | 83 +- pkg/security/tests/testopts.go | 4 +- ...-security-imds-event-4369a6ca5a4a97a6.yaml | 11 + 67 files changed, 3368 insertions(+), 201 deletions(-) create mode 100644 pkg/security/ebpf/c/include/helpers/imds.h create mode 100644 pkg/security/ebpf/c/include/hooks/network/imds.h create mode 100644 pkg/security/security_profile/activity_tree/imds_node.go create mode 100644 pkg/security/tests/imds_test.go create mode 100644 pkg/security/tests/imds_utils/imds_utils.go create mode 100644 pkg/security/tests/schemas/imds.schema.json create mode 100644 releasenotes/notes/runtime-security-imds-event-4369a6ca5a4a97a6.yaml diff --git a/docs/cloud-workload-security/agent_expressions.md b/docs/cloud-workload-security/agent_expressions.md index 7fca8a86e06e7d..2b5b5b618191a4 100644 --- a/docs/cloud-workload-security/agent_expressions.md +++ b/docs/cloud-workload-security/agent_expressions.md @@ -41,6 +41,7 @@ Triggers are events that correspond to types of activity seen by the system. The | `dns` | Network | A DNS request was sent | 7.36 | | `exec` | Process | A process was executed or forked | 7.27 | | `exit` | Process | A process was terminated | 7.38 | +| `imds` | Network | An IMDS event was captured | 7.55 | | `link` | File | Create a new name/alias for a file | 7.27 | | `load_module` | Kernel | A new kernel module was loaded | 7.35 | | `mkdir` | File | A directory was created | 7.27 | @@ -720,6 +721,21 @@ A process was terminated | [`exit.user_session.k8s_uid`](#common-usersessioncontext-k8s_uid-doc) | Kubernetes UID of the user that executed the process | | [`exit.user_session.k8s_username`](#common-usersessioncontext-k8s_username-doc) | Kubernetes username of the user that executed the process | +### Event `imds` + +An IMDS event was captured + +| Property | Definition | +| -------- | ------------- | +| [`imds.aws.is_imds_v2`](#imds-aws-is_imds_v2-doc) | a boolean which specifies if the IMDS event follows IMDSv1 or IMDSv2 conventions | +| [`imds.aws.security_credentials.type`](#imds-aws-security_credentials-type-doc) | the security credentials type | +| [`imds.cloud_provider`](#imds-cloud_provider-doc) | the intended cloud provider of the IMDS event | +| [`imds.host`](#imds-host-doc) | the host of the HTTP protocol | +| [`imds.server`](#imds-server-doc) | the server header of a response | +| [`imds.type`](#imds-type-doc) | the type of IMDS event | +| [`imds.url`](#imds-url-doc) | the queried IMDS URL | +| [`imds.user_agent`](#imds-user_agent-doc) | the user agent of the HTTP client | + ### Event `link` Create a new name/alias for a file @@ -2546,6 +2562,62 @@ Definition: Exit code of the process or number of the signal that caused the pro +### `imds.aws.is_imds_v2` {#imds-aws-is_imds_v2-doc} +Type: bool + +Definition: a boolean which specifies if the IMDS event follows IMDSv1 or IMDSv2 conventions + + + +### `imds.aws.security_credentials.type` {#imds-aws-security_credentials-type-doc} +Type: string + +Definition: the security credentials type + + + +### `imds.cloud_provider` {#imds-cloud_provider-doc} +Type: string + +Definition: the intended cloud provider of the IMDS event + + + +### `imds.host` {#imds-host-doc} +Type: string + +Definition: the host of the HTTP protocol + + + +### `imds.server` {#imds-server-doc} +Type: string + +Definition: the server header of a response + + + +### `imds.type` {#imds-type-doc} +Type: string + +Definition: the type of IMDS event + + + +### `imds.url` {#imds-url-doc} +Type: string + +Definition: the queried IMDS URL + + + +### `imds.user_agent` {#imds-user_agent-doc} +Type: string + +Definition: the user agent of the HTTP client + + + ### `load_module.args` {#load_module-args-doc} Type: string diff --git a/docs/cloud-workload-security/backend.md b/docs/cloud-workload-security/backend.md index 185afe81cf1746..734ab8652a5e72 100644 --- a/docs/cloud-workload-security/backend.md +++ b/docs/cloud-workload-security/backend.md @@ -18,6 +18,58 @@ CSM Threats logs have the following JSON schema: { "$id": "https://github.com/DataDog/datadog-agent/tree/main/pkg/security/serializers", "$defs": { + "AWSIMDSEvent": { + "properties": { + "is_imds_v2": { + "type": "boolean", + "description": "is_imds_v2 reports if the IMDS event follows IMDSv1 or IMDSv2 conventions" + }, + "security_credentials": { + "$ref": "#/$defs/AWSSecurityCredentials", + "description": "SecurityCredentials holds the scrubbed data collected on the security credentials" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "is_imds_v2" + ], + "description": "AWSIMDSEventSerializer serializes an AWS IMDS event to JSON" + }, + "AWSSecurityCredentials": { + "properties": { + "code": { + "type": "string", + "description": "code is the IMDS server code response" + }, + "type": { + "type": "string", + "description": "type is the security credentials type" + }, + "access_key_id": { + "type": "string", + "description": "access_key_id is the unique access key ID of the credentials" + }, + "last_updated": { + "type": "string", + "description": "last_updated is the last time the credentials were updated" + }, + "expiration": { + "type": "string", + "description": "expiration is the expiration date of the credentials" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "code", + "type", + "access_key_id", + "last_updated", + "expiration" + ], + "description": "AWSSecurityCredentialsSerializer serializes the security credentials from an AWS IMDS request" + }, "AgentContext": { "properties": { "rule_id": { @@ -533,6 +585,45 @@ CSM Threats logs have the following JSON schema: ], "description": "FileEventSerializer serializes a file event to JSON" }, + "IMDSEvent": { + "properties": { + "type": { + "type": "string", + "description": "type is the type of IMDS event" + }, + "cloud_provider": { + "type": "string", + "description": "cloud_provider is the intended cloud provider of the IMDS event" + }, + "url": { + "type": "string", + "description": "url is the url of the IMDS request" + }, + "host": { + "type": "string", + "description": "host is the host of the HTTP protocol" + }, + "user_agent": { + "type": "string", + "description": "user_agent is the user agent of the HTTP client" + }, + "server": { + "type": "string", + "description": "server is the server header of a response" + }, + "aws": { + "$ref": "#/$defs/AWSIMDSEvent", + "description": "AWS holds the AWS specific data parsed from the IMDS event" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "cloud_provider" + ], + "description": "IMDSEventSerializer serializes an IMDS event to JSON" + }, "IPPort": { "properties": { "ip": { @@ -960,6 +1051,13 @@ CSM Threats logs have the following JSON schema: "syscalls": { "$ref": "#/$defs/SyscallsEvent", "description": "List of syscalls captured to generate the event" + }, + "aws_security_credentials": { + "items": { + "$ref": "#/$defs/AWSSecurityCredentials" + }, + "type": "array", + "description": "List of AWS Security Credentials that the process had access to" } }, "additionalProperties": false, @@ -1093,6 +1191,13 @@ CSM Threats logs have the following JSON schema: "$ref": "#/$defs/SyscallsEvent", "description": "List of syscalls captured to generate the event" }, + "aws_security_credentials": { + "items": { + "$ref": "#/$defs/AWSSecurityCredentials" + }, + "type": "array", + "description": "List of AWS Security Credentials that the process had access to" + }, "parent": { "$ref": "#/$defs/Process", "description": "Parent process" @@ -1477,6 +1582,9 @@ CSM Threats logs have the following JSON schema: "dns": { "$ref": "#/$defs/DNSEvent" }, + "imds": { + "$ref": "#/$defs/IMDSEvent" + }, "bind": { "$ref": "#/$defs/BindEvent" }, @@ -1522,11 +1630,96 @@ CSM Threats logs have the following JSON schema: | `signal` | $ref | Please see [SignalEvent](#signalevent) | | `splice` | $ref | Please see [SpliceEvent](#spliceevent) | | `dns` | $ref | Please see [DNSEvent](#dnsevent) | +| `imds` | $ref | Please see [IMDSEvent](#imdsevent) | | `bind` | $ref | Please see [BindEvent](#bindevent) | | `mount` | $ref | Please see [MountEvent](#mountevent) | | `syscalls` | $ref | Please see [SyscallsEvent](#syscallsevent) | | `usr` | $ref | Please see [UserContext](#usercontext) | +## `AWSIMDSEvent` + + +{{< code-block lang="json" collapsible="true" >}} +{ + "properties": { + "is_imds_v2": { + "type": "boolean", + "description": "is_imds_v2 reports if the IMDS event follows IMDSv1 or IMDSv2 conventions" + }, + "security_credentials": { + "$ref": "#/$defs/AWSSecurityCredentials", + "description": "SecurityCredentials holds the scrubbed data collected on the security credentials" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "is_imds_v2" + ], + "description": "AWSIMDSEventSerializer serializes an AWS IMDS event to JSON" +} + +{{< /code-block >}} + +| Field | Description | +| ----- | ----------- | +| `is_imds_v2` | is_imds_v2 reports if the IMDS event follows IMDSv1 or IMDSv2 conventions | +| `security_credentials` | SecurityCredentials holds the scrubbed data collected on the security credentials | + +| References | +| ---------- | +| [AWSSecurityCredentials](#awssecuritycredentials) | + +## `AWSSecurityCredentials` + + +{{< code-block lang="json" collapsible="true" >}} +{ + "properties": { + "code": { + "type": "string", + "description": "code is the IMDS server code response" + }, + "type": { + "type": "string", + "description": "type is the security credentials type" + }, + "access_key_id": { + "type": "string", + "description": "access_key_id is the unique access key ID of the credentials" + }, + "last_updated": { + "type": "string", + "description": "last_updated is the last time the credentials were updated" + }, + "expiration": { + "type": "string", + "description": "expiration is the expiration date of the credentials" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "code", + "type", + "access_key_id", + "last_updated", + "expiration" + ], + "description": "AWSSecurityCredentialsSerializer serializes the security credentials from an AWS IMDS request" +} + +{{< /code-block >}} + +| Field | Description | +| ----- | ----------- | +| `code` | code is the IMDS server code response | +| `type` | type is the security credentials type | +| `access_key_id` | access_key_id is the unique access key ID of the credentials | +| `last_updated` | last_updated is the last time the credentials were updated | +| `expiration` | expiration is the expiration date of the credentials | + + ## `AgentContext` @@ -2287,6 +2480,66 @@ CSM Threats logs have the following JSON schema: | ---------- | | [File](#file) | +## `IMDSEvent` + + +{{< code-block lang="json" collapsible="true" >}} +{ + "properties": { + "type": { + "type": "string", + "description": "type is the type of IMDS event" + }, + "cloud_provider": { + "type": "string", + "description": "cloud_provider is the intended cloud provider of the IMDS event" + }, + "url": { + "type": "string", + "description": "url is the url of the IMDS request" + }, + "host": { + "type": "string", + "description": "host is the host of the HTTP protocol" + }, + "user_agent": { + "type": "string", + "description": "user_agent is the user agent of the HTTP client" + }, + "server": { + "type": "string", + "description": "server is the server header of a response" + }, + "aws": { + "$ref": "#/$defs/AWSIMDSEvent", + "description": "AWS holds the AWS specific data parsed from the IMDS event" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "cloud_provider" + ], + "description": "IMDSEventSerializer serializes an IMDS event to JSON" +} + +{{< /code-block >}} + +| Field | Description | +| ----- | ----------- | +| `type` | type is the type of IMDS event | +| `cloud_provider` | cloud_provider is the intended cloud provider of the IMDS event | +| `url` | url is the url of the IMDS request | +| `host` | host is the host of the HTTP protocol | +| `user_agent` | user_agent is the user agent of the HTTP client | +| `server` | server is the server header of a response | +| `aws` | AWS holds the AWS specific data parsed from the IMDS event | + +| References | +| ---------- | +| [AWSIMDSEvent](#awsimdsevent) | + ## `IPPort` @@ -2884,6 +3137,13 @@ CSM Threats logs have the following JSON schema: "syscalls": { "$ref": "#/$defs/SyscallsEvent", "description": "List of syscalls captured to generate the event" + }, + "aws_security_credentials": { + "items": { + "$ref": "#/$defs/AWSSecurityCredentials" + }, + "type": "array", + "description": "List of AWS Security Credentials that the process had access to" } }, "additionalProperties": false, @@ -2927,6 +3187,7 @@ CSM Threats logs have the following JSON schema: | `is_exec_child` | Indicates whether the process is an exec following another exec | | `source` | Process source | | `syscalls` | List of syscalls captured to generate the event | +| `aws_security_credentials` | List of AWS Security Credentials that the process had access to | | References | | ---------- | @@ -3064,6 +3325,13 @@ CSM Threats logs have the following JSON schema: "$ref": "#/$defs/SyscallsEvent", "description": "List of syscalls captured to generate the event" }, + "aws_security_credentials": { + "items": { + "$ref": "#/$defs/AWSSecurityCredentials" + }, + "type": "array", + "description": "List of AWS Security Credentials that the process had access to" + }, "parent": { "$ref": "#/$defs/Process", "description": "Parent process" @@ -3121,6 +3389,7 @@ CSM Threats logs have the following JSON schema: | `is_exec_child` | Indicates whether the process is an exec following another exec | | `source` | Process source | | `syscalls` | List of syscalls captured to generate the event | +| `aws_security_credentials` | List of AWS Security Credentials that the process had access to | | `parent` | Parent process | | `ancestors` | Ancestor processes | | `variables` | Variables values | diff --git a/docs/cloud-workload-security/backend.schema.json b/docs/cloud-workload-security/backend.schema.json index 3cf301320e9f5b..028408bfdc9c74 100644 --- a/docs/cloud-workload-security/backend.schema.json +++ b/docs/cloud-workload-security/backend.schema.json @@ -2,6 +2,58 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://github.com/DataDog/datadog-agent/tree/main/pkg/security/serializers", "$defs": { + "AWSIMDSEvent": { + "properties": { + "is_imds_v2": { + "type": "boolean", + "description": "is_imds_v2 reports if the IMDS event follows IMDSv1 or IMDSv2 conventions" + }, + "security_credentials": { + "$ref": "#/$defs/AWSSecurityCredentials", + "description": "SecurityCredentials holds the scrubbed data collected on the security credentials" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "is_imds_v2" + ], + "description": "AWSIMDSEventSerializer serializes an AWS IMDS event to JSON" + }, + "AWSSecurityCredentials": { + "properties": { + "code": { + "type": "string", + "description": "code is the IMDS server code response" + }, + "type": { + "type": "string", + "description": "type is the security credentials type" + }, + "access_key_id": { + "type": "string", + "description": "access_key_id is the unique access key ID of the credentials" + }, + "last_updated": { + "type": "string", + "description": "last_updated is the last time the credentials were updated" + }, + "expiration": { + "type": "string", + "description": "expiration is the expiration date of the credentials" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "code", + "type", + "access_key_id", + "last_updated", + "expiration" + ], + "description": "AWSSecurityCredentialsSerializer serializes the security credentials from an AWS IMDS request" + }, "AgentContext": { "properties": { "rule_id": { @@ -517,6 +569,45 @@ ], "description": "FileEventSerializer serializes a file event to JSON" }, + "IMDSEvent": { + "properties": { + "type": { + "type": "string", + "description": "type is the type of IMDS event" + }, + "cloud_provider": { + "type": "string", + "description": "cloud_provider is the intended cloud provider of the IMDS event" + }, + "url": { + "type": "string", + "description": "url is the url of the IMDS request" + }, + "host": { + "type": "string", + "description": "host is the host of the HTTP protocol" + }, + "user_agent": { + "type": "string", + "description": "user_agent is the user agent of the HTTP client" + }, + "server": { + "type": "string", + "description": "server is the server header of a response" + }, + "aws": { + "$ref": "#/$defs/AWSIMDSEvent", + "description": "AWS holds the AWS specific data parsed from the IMDS event" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "cloud_provider" + ], + "description": "IMDSEventSerializer serializes an IMDS event to JSON" + }, "IPPort": { "properties": { "ip": { @@ -944,6 +1035,13 @@ "syscalls": { "$ref": "#/$defs/SyscallsEvent", "description": "List of syscalls captured to generate the event" + }, + "aws_security_credentials": { + "items": { + "$ref": "#/$defs/AWSSecurityCredentials" + }, + "type": "array", + "description": "List of AWS Security Credentials that the process had access to" } }, "additionalProperties": false, @@ -1077,6 +1175,13 @@ "$ref": "#/$defs/SyscallsEvent", "description": "List of syscalls captured to generate the event" }, + "aws_security_credentials": { + "items": { + "$ref": "#/$defs/AWSSecurityCredentials" + }, + "type": "array", + "description": "List of AWS Security Credentials that the process had access to" + }, "parent": { "$ref": "#/$defs/Process", "description": "Parent process" @@ -1461,6 +1566,9 @@ "dns": { "$ref": "#/$defs/DNSEvent" }, + "imds": { + "$ref": "#/$defs/IMDSEvent" + }, "bind": { "$ref": "#/$defs/BindEvent" }, diff --git a/docs/cloud-workload-security/secl.json b/docs/cloud-workload-security/secl.json index 49100e0a38445a..c2a7f67f912195 100644 --- a/docs/cloud-workload-security/secl.json +++ b/docs/cloud-workload-security/secl.json @@ -2515,6 +2515,55 @@ } ] }, + { + "name": "imds", + "definition": "An IMDS event was captured", + "type": "Network", + "from_agent_version": "7.55", + "experimental": false, + "properties": [ + { + "name": "imds.aws.is_imds_v2", + "definition": "a boolean which specifies if the IMDS event follows IMDSv1 or IMDSv2 conventions", + "property_doc_link": "imds-aws-is_imds_v2-doc" + }, + { + "name": "imds.aws.security_credentials.type", + "definition": "the security credentials type", + "property_doc_link": "imds-aws-security_credentials-type-doc" + }, + { + "name": "imds.cloud_provider", + "definition": "the intended cloud provider of the IMDS event", + "property_doc_link": "imds-cloud_provider-doc" + }, + { + "name": "imds.host", + "definition": "the host of the HTTP protocol", + "property_doc_link": "imds-host-doc" + }, + { + "name": "imds.server", + "definition": "the server header of a response", + "property_doc_link": "imds-server-doc" + }, + { + "name": "imds.type", + "definition": "the type of IMDS event", + "property_doc_link": "imds-type-doc" + }, + { + "name": "imds.url", + "definition": "the queried IMDS URL", + "property_doc_link": "imds-url-doc" + }, + { + "name": "imds.user_agent", + "definition": "the user agent of the HTTP client", + "property_doc_link": "imds-user_agent-doc" + } + ] + }, { "name": "link", "definition": "Create a new name/alias for a file", @@ -8938,6 +8987,102 @@ "constants_link": "", "examples": [] }, + { + "name": "imds.aws.is_imds_v2", + "link": "imds-aws-is_imds_v2-doc", + "type": "bool", + "definition": "a boolean which specifies if the IMDS event follows IMDSv1 or IMDSv2 conventions", + "prefixes": [ + "imds.aws" + ], + "constants": "", + "constants_link": "", + "examples": [] + }, + { + "name": "imds.aws.security_credentials.type", + "link": "imds-aws-security_credentials-type-doc", + "type": "string", + "definition": "the security credentials type", + "prefixes": [ + "imds.aws.security_credentials" + ], + "constants": "", + "constants_link": "", + "examples": [] + }, + { + "name": "imds.cloud_provider", + "link": "imds-cloud_provider-doc", + "type": "string", + "definition": "the intended cloud provider of the IMDS event", + "prefixes": [ + "imds" + ], + "constants": "", + "constants_link": "", + "examples": [] + }, + { + "name": "imds.host", + "link": "imds-host-doc", + "type": "string", + "definition": "the host of the HTTP protocol", + "prefixes": [ + "imds" + ], + "constants": "", + "constants_link": "", + "examples": [] + }, + { + "name": "imds.server", + "link": "imds-server-doc", + "type": "string", + "definition": "the server header of a response", + "prefixes": [ + "imds" + ], + "constants": "", + "constants_link": "", + "examples": [] + }, + { + "name": "imds.type", + "link": "imds-type-doc", + "type": "string", + "definition": "the type of IMDS event", + "prefixes": [ + "imds" + ], + "constants": "", + "constants_link": "", + "examples": [] + }, + { + "name": "imds.url", + "link": "imds-url-doc", + "type": "string", + "definition": "the queried IMDS URL", + "prefixes": [ + "imds" + ], + "constants": "", + "constants_link": "", + "examples": [] + }, + { + "name": "imds.user_agent", + "link": "imds-user_agent-doc", + "type": "string", + "definition": "the user agent of the HTTP client", + "prefixes": [ + "imds" + ], + "constants": "", + "constants_link": "", + "examples": [] + }, { "name": "load_module.args", "link": "load_module-args-doc", diff --git a/pkg/config/setup/system_probe.go b/pkg/config/setup/system_probe.go index 5cb193b9769696..6564ea61ced864 100644 --- a/pkg/config/setup/system_probe.go +++ b/pkg/config/setup/system_probe.go @@ -354,6 +354,7 @@ func InitSystemProbeConfig(cfg pkgconfigmodel.Config) { eventMonitorBindEnvAndSetDefault(cfg, join(evNS, "runtime_compilation.enabled"), false) eventMonitorBindEnv(cfg, join(evNS, "runtime_compilation.compiled_constants_enabled")) eventMonitorBindEnvAndSetDefault(cfg, join(evNS, "network.enabled"), true) + eventMonitorBindEnvAndSetDefault(cfg, join(evNS, "network.ingress.enabled"), false) eventMonitorBindEnvAndSetDefault(cfg, join(evNS, "events_stats.polling_interval"), 20) eventMonitorBindEnvAndSetDefault(cfg, join(evNS, "syscalls_monitor.enabled"), false) cfg.BindEnvAndSetDefault(join(evNS, "socket"), defaultEventMonitorAddress) diff --git a/pkg/config/setup/system_probe_cws.go b/pkg/config/setup/system_probe_cws.go index 1c409a20e3fedb..c606da896caf22 100644 --- a/pkg/config/setup/system_probe_cws.go +++ b/pkg/config/setup/system_probe_cws.go @@ -50,7 +50,7 @@ func initCWSSystemProbeConfig(cfg pkgconfigmodel.Config) { cfg.BindEnvAndSetDefault("runtime_security_config.activity_dump.min_timeout", "10m") cfg.BindEnvAndSetDefault("runtime_security_config.activity_dump.max_dump_size", 1750) cfg.BindEnvAndSetDefault("runtime_security_config.activity_dump.traced_cgroups_count", 5) - cfg.BindEnvAndSetDefault("runtime_security_config.activity_dump.traced_event_types", []string{"exec", "open", "dns"}) + cfg.BindEnvAndSetDefault("runtime_security_config.activity_dump.traced_event_types", []string{"exec", "open", "dns", "imds"}) cfg.BindEnv("runtime_security_config.activity_dump.cgroup_dump_timeout") // deprecated in favor of dump_duration cfg.BindEnvAndSetDefault("runtime_security_config.activity_dump.dump_duration", "900s") cfg.BindEnvAndSetDefault("runtime_security_config.activity_dump.rate_limiter", 500) @@ -87,7 +87,7 @@ func initCWSSystemProbeConfig(cfg pkgconfigmodel.Config) { cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.auto_suppression.event_types", []string{"exec", "dns"}) // CWS - Anomaly detection - cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.event_types", []string{"exec"}) + cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.event_types", []string{"exec", "imds"}) cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.default_minimum_stable_period", "900s") cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.minimum_stable_period.exec", "900s") cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.minimum_stable_period.dns", "900s") @@ -118,6 +118,9 @@ func initCWSSystemProbeConfig(cfg pkgconfigmodel.Config) { cfg.BindEnvAndSetDefault("runtime_security_config.ebpfless.enabled", false) cfg.BindEnvAndSetDefault("runtime_security_config.ebpfless.socket", constants.DefaultEBPFLessProbeAddr) + // CWS - IMDS + cfg.BindEnvAndSetDefault("runtime_security_config.imds_ipv4", "169.254.169.254") + // CWS enforcement capabilities cfg.BindEnvAndSetDefault("runtime_security_config.enforcement.enabled", true) } diff --git a/pkg/security/config/config.go b/pkg/security/config/config.go index 3895c3bb5a8c3d..ea5d95c7da0797 100644 --- a/pkg/security/config/config.go +++ b/pkg/security/config/config.go @@ -7,7 +7,9 @@ package config import ( + "encoding/binary" "fmt" + "net" "strings" "time" @@ -229,9 +231,11 @@ type RuntimeSecurityConfig struct { //WindowsFilenameCacheSize is the max number of filenames to cache WindowsFilenameCacheSize int - //WindowsRegistryCacheSize is the max number of registry paths to cache WindowsRegistryCacheSize int + + // IMDSIPv4 is used to provide a custom IP address for the IMDS endpoint + IMDSIPv4 uint32 } // Config defines a security config @@ -395,6 +399,9 @@ func NewRuntimeSecurityConfig() (*RuntimeSecurityConfig, error) { // ebpf less EBPFLessEnabled: coreconfig.SystemProbe.GetBool("runtime_security_config.ebpfless.enabled"), EBPFLessSocket: coreconfig.SystemProbe.GetString("runtime_security_config.ebpfless.socket"), + + // IMDS + IMDSIPv4: parseIMDSIPv4(), } if err := rsConfig.sanitize(); err != nil { @@ -409,6 +416,16 @@ func (c *RuntimeSecurityConfig) IsRuntimeEnabled() bool { return c.RuntimeEnabled || c.FIMEnabled } +// parseIMDSIPv4 returns the uint32 representation of the IMDS IP set by the configuration +func parseIMDSIPv4() uint32 { + ip := coreconfig.SystemProbe.GetString("runtime_security_config.imds_ipv4") + parsedIP := net.ParseIP(ip) + if parsedIP == nil { + return 0 + } + return binary.LittleEndian.Uint32(parsedIP.To4()) +} + // If RC is globally enabled, RC is enabled for CWS, unless the CWS-specific RC value is explicitly set to false func isRemoteConfigEnabled() bool { // This value defaults to true @@ -440,10 +457,14 @@ func (c *RuntimeSecurityConfig) sanitize() error { c.HostServiceName = fmt.Sprintf("service:%s", serviceName) } + if c.IMDSIPv4 == 0 { + return fmt.Errorf("invalid IPv4 address: got %v", coreconfig.SystemProbe.GetString("runtime_security_config.imds_ipv4")) + } + return c.sanitizeRuntimeSecurityConfigActivityDump() } -// sanitizeNetworkConfiguration ensures that runtime_security_config.activity_dump is properly configured +// sanitizeRuntimeSecurityConfigActivityDump ensures that runtime_security_config.activity_dump is properly configured func (c *RuntimeSecurityConfig) sanitizeRuntimeSecurityConfigActivityDump() error { var execFound bool for _, evtType := range c.ActivityDumpTracedEventTypes { diff --git a/pkg/security/ebpf/c/include/constants/custom.h b/pkg/security/ebpf/c/include/constants/custom.h index f92b30a0f25104..bc1eb62efdfc8b 100644 --- a/pkg/security/ebpf/c/include/constants/custom.h +++ b/pkg/security/ebpf/c/include/constants/custom.h @@ -65,16 +65,23 @@ enum DENTRY_ERPC_RESOLUTION_CODE { DR_ERPC_UNKNOWN_ERROR, }; +enum TC_TAIL_CALL_KEYS { + UNKNOWN, + DNS_REQUEST, + DNS_REQUEST_PARSER, + IMDS_REQUEST, +}; + #define DNS_MAX_LENGTH 256 #define DNS_EVENT_KEY 0 -#define DNS_REQUEST 1 -#define DNS_REQUEST_PARSER 2 #define EGRESS 1 #define INGRESS 2 #define ACT_OK TC_ACT_UNSPEC #define ACT_SHOT TC_ACT_SHOT #define PACKET_KEY 0 +#define IMDS_EVENT_KEY 0 +#define IMDS_MAX_LENGTH 2048 #define STATE_NULL 0 #define STATE_NEWLINK 1 @@ -174,4 +181,10 @@ static __attribute__((always_inline)) u64 is_anomaly_syscalls_enabled() { return anomaly; }; +static __attribute__((always_inline)) u64 get_imds_ip() { + u64 imds_ip; + LOAD_CONSTANT("imds_ip", imds_ip); + return imds_ip; +}; + #endif diff --git a/pkg/security/ebpf/c/include/constants/enums.h b/pkg/security/ebpf/c/include/constants/enums.h index 892f8598b8c768..b81381d45bb176 100644 --- a/pkg/security/ebpf/c/include/constants/enums.h +++ b/pkg/security/ebpf/c/include/constants/enums.h @@ -45,6 +45,7 @@ enum event_type { EVENT_BIND, EVENT_UNSHARE_MNTNS, EVENT_SYSCALLS, + EVENT_IMDS, EVENT_MAX, // has to be the last one EVENT_ALL = 0xffffffff // used as a mask for all the events diff --git a/pkg/security/ebpf/c/include/events_definition.h b/pkg/security/ebpf/c/include/events_definition.h index c322e2454b12dd..e8494dd6911c17 100644 --- a/pkg/security/ebpf/c/include/events_definition.h +++ b/pkg/security/ebpf/c/include/events_definition.h @@ -163,6 +163,16 @@ struct dns_event_t { char name[DNS_MAX_LENGTH]; }; +struct imds_event_t { + struct kevent_t event; + struct process_context_t process; + struct span_context_t span; + struct container_context_t container; + struct network_context_t network; + + u8 body[IMDS_MAX_LENGTH]; +}; + struct link_event_t { struct kevent_t event; struct process_context_t process; diff --git a/pkg/security/ebpf/c/include/helpers/all.h b/pkg/security/ebpf/c/include/helpers/all.h index 256dd43e876d34..e3a31a2d3229b1 100644 --- a/pkg/security/ebpf/c/include/helpers/all.h +++ b/pkg/security/ebpf/c/include/helpers/all.h @@ -9,6 +9,7 @@ #include "dentry_resolver.h" #include "discaders.h" #include "dns.h" +#include "imds.h" #include "erpc.h" #include "events.h" #include "events_predicates.h" diff --git a/pkg/security/ebpf/c/include/helpers/dns.h b/pkg/security/ebpf/c/include/helpers/dns.h index d0edac2971c58c..9d97d3a3d56dc2 100644 --- a/pkg/security/ebpf/c/include/helpers/dns.h +++ b/pkg/security/ebpf/c/include/helpers/dns.h @@ -24,6 +24,7 @@ __attribute__((always_inline)) struct dns_event_t *reset_dns_event(struct __sk_b // reset DNS name evt->name[0] = 0; evt->size = pkt->payload_len; + evt->event.flags = 0; // process context fill_network_process_context(&evt->process, pkt); diff --git a/pkg/security/ebpf/c/include/helpers/imds.h b/pkg/security/ebpf/c/include/helpers/imds.h new file mode 100644 index 00000000000000..c53b53c15f9e0f --- /dev/null +++ b/pkg/security/ebpf/c/include/helpers/imds.h @@ -0,0 +1,50 @@ +#ifndef _HELPERS_IMDS_H +#define _HELPERS_IMDS_H + +#include "constants/enums.h" +#include "maps.h" + +#include "container.h" +#include "network.h" +#include "process.h" + +__attribute__((always_inline)) struct imds_event_t *get_imds_event() { + u32 key = IMDS_EVENT_KEY; + return bpf_map_lookup_elem(&imds_event, &key); +} + +__attribute__((always_inline)) struct imds_event_t *reset_imds_event(struct __sk_buff *skb, struct packet_t *pkt) { + struct imds_event_t *evt = get_imds_event(); + if (evt == NULL) { + // should never happen + return NULL; + } + + // reset event flags + evt->event.flags = 0; + + // process context + fill_network_process_context(&evt->process, pkt); + + // network context + fill_network_context(&evt->network, skb, pkt); + + struct proc_cache_t *entry = get_proc_cache(evt->process.pid); + if (entry == NULL) { + evt->container.container_id[0] = 0; + } else { + copy_container_id_no_tracing(entry->container.container_id, &evt->container.container_id); + } + + // should we sample this event for activity dumps ? + struct activity_dump_config *config = lookup_or_delete_traced_pid(evt->process.pid, bpf_ktime_get_ns(), NULL); + if (config) { + if (mask_has_event(config->event_mask, EVENT_IMDS)) { + evt->event.flags |= EVENT_FLAGS_ACTIVITY_DUMP_SAMPLE; + } + } + + return evt; +} + +#endif diff --git a/pkg/security/ebpf/c/include/hooks/all.h b/pkg/security/ebpf/c/include/hooks/all.h index fb3568d45997eb..ca6c97047f7b7a 100644 --- a/pkg/security/ebpf/c/include/hooks/all.h +++ b/pkg/security/ebpf/c/include/hooks/all.h @@ -38,6 +38,7 @@ #ifndef DO_NOT_USE_TC #include "network/dns.h" +#include "network/imds.h" #include "network/flow.h" #include "network/net_device.h" #include "network/router.h" diff --git a/pkg/security/ebpf/c/include/hooks/network/imds.h b/pkg/security/ebpf/c/include/hooks/network/imds.h new file mode 100644 index 00000000000000..b0b72559bf8ea4 --- /dev/null +++ b/pkg/security/ebpf/c/include/hooks/network/imds.h @@ -0,0 +1,36 @@ +#ifndef _HOOKS_NETWORK_IMDS_H_ +#define _HOOKS_NETWORK_IMDS_H_ + +#include "helpers/imds.h" +#include "helpers/network.h" +#include "perf_ring.h" + +SEC("classifier/imds_request") +int classifier_imds_request(struct __sk_buff *skb) { + struct packet_t *pkt = get_packet(); + if (pkt == NULL) { + // should never happen + return ACT_OK; + } + + struct imds_event_t *evt = reset_imds_event(skb, pkt); + if (evt == NULL || skb == NULL) { + // should never happen + return ACT_OK; + } + + pkt->payload_len = pkt->payload_len & (IMDS_MAX_LENGTH - 1); + if (pkt->payload_len > 1) { + // copy IMDS request + if (bpf_skb_load_bytes(skb, pkt->offset, evt->body, pkt->payload_len) < 0) { + return ACT_OK; + } + + send_event_with_size_ptr(skb, EVENT_IMDS, evt, offsetof(struct imds_event_t, body) + (pkt->payload_len & (IMDS_MAX_LENGTH - 1))); + } + + // done + return ACT_OK; +} + +#endif diff --git a/pkg/security/ebpf/c/include/hooks/network/router.h b/pkg/security/ebpf/c/include/hooks/network/router.h index 7f7887e9e92e12..24ec1f6b2b9e93 100644 --- a/pkg/security/ebpf/c/include/hooks/network/router.h +++ b/pkg/security/ebpf/c/include/hooks/network/router.h @@ -36,17 +36,23 @@ __attribute__((always_inline)) int route_pkt(struct __sk_buff *skb, struct packe pid_route.addr[1] = pkt->translated_ns_flow.flow.daddr[1]; pid_route.port = pkt->translated_ns_flow.flow.dport; pid_route.netns = pkt->translated_ns_flow.netns; + break; } } pkt->pid = get_flow_pid(&pid_route); // TODO: l3 / l4 firewall - // route l7 protocol + // route DNS requests if (pkt->l4_protocol == IPPROTO_UDP && pkt->translated_ns_flow.flow.dport == htons(53)) { tail_call_to_classifier(skb, DNS_REQUEST); } + // route IMDS requests + if (pkt->l4_protocol == IPPROTO_TCP && ((pkt->ns_flow.flow.saddr[0] & 0xFFFFFFFF) == get_imds_ip() || (pkt->ns_flow.flow.daddr[0] & 0xFFFFFFFF) == get_imds_ip() )) { + tail_call_to_classifier(skb, IMDS_REQUEST); + } + return ACT_OK; } diff --git a/pkg/security/ebpf/c/include/hooks/network/tc.h b/pkg/security/ebpf/c/include/hooks/network/tc.h index 045b4513c3fdee..f46b39d8055459 100644 --- a/pkg/security/ebpf/c/include/hooks/network/tc.h +++ b/pkg/security/ebpf/c/include/hooks/network/tc.h @@ -5,13 +5,7 @@ #include "router.h" -SEC("classifier/ingress") -int classifier_ingress(struct __sk_buff *skb) { - return ACT_OK; -}; - -SEC("classifier/egress") -int classifier_egress(struct __sk_buff *skb) { +__attribute__((always_inline)) int parse_packet(struct __sk_buff *skb, int network_direction) { struct cursor c = {}; tc_cursor_init(&c, skb); @@ -99,7 +93,17 @@ int classifier_egress(struct __sk_buff *skb) { return ACT_OK; } - return route_pkt(skb, pkt, EGRESS); + return route_pkt(skb, pkt, network_direction); +}; + +SEC("classifier/ingress") +int classifier_ingress(struct __sk_buff *skb) { + return parse_packet(skb, INGRESS); +}; + +SEC("classifier/egress") +int classifier_egress(struct __sk_buff *skb) { + return parse_packet(skb, EGRESS); }; #endif diff --git a/pkg/security/ebpf/c/include/hooks/procfs.h b/pkg/security/ebpf/c/include/hooks/procfs.h index 34f3da246fa4bf..34b45ebc587a6a 100644 --- a/pkg/security/ebpf/c/include/hooks/procfs.h +++ b/pkg/security/ebpf/c/include/hooks/procfs.h @@ -101,6 +101,9 @@ int hook_path_get(ctx_t *ctx) { return 0; } bpf_probe_read(&route.port, sizeof(route.port), &sk->__sk_common.skc_num); + // Calling htons is necessary to support snapshotted bound port. Without it, we're can't properly route incoming + // traffic to the relevant process. + route.port = htons(route.port); // save pid route u32 pid = *procfs_pid; diff --git a/pkg/security/ebpf/c/include/maps.h b/pkg/security/ebpf/c/include/maps.h index f8c39691330c24..76c584e2649cec 100644 --- a/pkg/security/ebpf/c/include/maps.h +++ b/pkg/security/ebpf/c/include/maps.h @@ -80,6 +80,7 @@ BPF_PERCPU_ARRAY_MAP(dr_erpc_stats_fb, struct dr_erpc_stats_t, 6) BPF_PERCPU_ARRAY_MAP(dr_erpc_stats_bb, struct dr_erpc_stats_t, 6) BPF_PERCPU_ARRAY_MAP(is_discarded_by_inode_gen, struct is_discarded_by_inode_t, 1) BPF_PERCPU_ARRAY_MAP(dns_event, struct dns_event_t, 1) +BPF_PERCPU_ARRAY_MAP(imds_event, struct imds_event_t, 1) BPF_PERCPU_ARRAY_MAP(packets, struct packet_t, 1) BPF_PERCPU_ARRAY_MAP(selinux_write_buffer, struct selinux_write_buffer_t, 1) BPF_PERCPU_ARRAY_MAP(is_new_kthread, u32, 1) diff --git a/pkg/security/ebpf/probes/all.go b/pkg/security/ebpf/probes/all.go index 374a7619291e99..3a513ebd5b7723 100644 --- a/pkg/security/ebpf/probes/all.go +++ b/pkg/security/ebpf/probes/all.go @@ -76,7 +76,7 @@ func AllProbes(fentry bool) []*manager.Probe { allProbes = append(allProbes, getSpliceProbes(fentry)...) allProbes = append(allProbes, getFlowProbes()...) allProbes = append(allProbes, getNetDeviceProbes()...) - allProbes = append(allProbes, GetTCProbes()...) + allProbes = append(allProbes, GetTCProbes(true)...) allProbes = append(allProbes, getBindProbes(fentry)...) allProbes = append(allProbes, getSyscallMonitorProbes()...) allProbes = append(allProbes, getChdirProbes(fentry)...) diff --git a/pkg/security/ebpf/probes/const.go b/pkg/security/ebpf/probes/const.go index 3fca0fa00dac9e..be567728ad94c0 100644 --- a/pkg/security/ebpf/probes/const.go +++ b/pkg/security/ebpf/probes/const.go @@ -72,10 +72,12 @@ const ( ) const ( - // TCDNSRequestKey is the key to DNS request program + // TCDNSRequestKey is the key to the DNS request program TCDNSRequestKey uint32 = iota + 1 - // TCDNSRequestParserKey is the key to DNS request parser program + // TCDNSRequestParserKey is the key to the DNS request parser program TCDNSRequestParserKey + // TCIMDSRequestParserKey is the key to the IMDS request program + TCIMDSRequestParserKey ) const ( diff --git a/pkg/security/ebpf/probes/event_types.go b/pkg/security/ebpf/probes/event_types.go index 1ad26896492722..57545b2c8fe5bd 100644 --- a/pkg/security/ebpf/probes/event_types.go +++ b/pkg/security/ebpf/probes/event_types.go @@ -443,6 +443,15 @@ func GetSelectorsPerEventType(fentry bool) map[eval.EventType][]manager.ProbesSe }}, }, + // List of probes required to capture IMDS events + "imds": { + &manager.AllOf{Selectors: []manager.ProbesSelector{ + &manager.AllOf{Selectors: NetworkSelectors()}, + &manager.AllOf{Selectors: NetworkVethSelectors()}, + kprobeOrFentry("security_socket_bind"), + }}, + }, + // List of probes required to capture chdir events "chdir": { &manager.AllOf{Selectors: []manager.ProbesSelector{ diff --git a/pkg/security/ebpf/probes/tc.go b/pkg/security/ebpf/probes/tc.go index 9e493ab0f7dab6..a0767d3e0442f5 100644 --- a/pkg/security/ebpf/probes/tc.go +++ b/pkg/security/ebpf/probes/tc.go @@ -14,27 +14,32 @@ import ( ) // GetTCProbes returns the list of TCProbes -func GetTCProbes() []*manager.Probe { - return []*manager.Probe{ +func GetTCProbes(withNetworkIngress bool) []*manager.Probe { + out := []*manager.Probe{ { ProbeIdentificationPair: manager.ProbeIdentificationPair{ UID: SecurityAgentUID, - EBPFFuncName: "classifier_ingress", + EBPFFuncName: "classifier_egress", }, - NetworkDirection: manager.Ingress, + NetworkDirection: manager.Egress, TCFilterProtocol: unix.ETH_P_ALL, KeepProgramSpec: true, }, - { + } + + if withNetworkIngress { + out = append(out, &manager.Probe{ ProbeIdentificationPair: manager.ProbeIdentificationPair{ UID: SecurityAgentUID, - EBPFFuncName: "classifier_egress", + EBPFFuncName: "classifier_ingress", }, - NetworkDirection: manager.Egress, + NetworkDirection: manager.Ingress, TCFilterProtocol: unix.ETH_P_ALL, KeepProgramSpec: true, - }, + }) } + + return out } // GetAllTCProgramFunctions returns the list of TC classifier sections @@ -42,9 +47,10 @@ func GetAllTCProgramFunctions() []string { output := []string{ "classifier_dns_request_parser", "classifier_dns_request", + "classifier_imds_request", } - for _, tcProbe := range GetTCProbes() { + for _, tcProbe := range GetTCProbes(true) { output = append(output, tcProbe.EBPFFuncName) } @@ -75,5 +81,12 @@ func getTCTailCallRoutes() []manager.TailCallRoute { EBPFFuncName: "classifier_dns_request_parser", }, }, + { + ProgArrayName: "classifier_router", + Key: TCIMDSRequestParserKey, + ProbeIdentificationPair: manager.ProbeIdentificationPair{ + EBPFFuncName: "classifier_imds_request", + }, + }, } } diff --git a/pkg/security/probe/config/config.go b/pkg/security/probe/config/config.go index 5981d31735e595..2fee84a2c98212 100644 --- a/pkg/security/probe/config/config.go +++ b/pkg/security/probe/config/config.go @@ -128,6 +128,8 @@ type Config struct { // NetworkEnabled defines if the network probes should be activated NetworkEnabled bool + // NetworkIngressEnabled defines if the network ingress probes should be activated + NetworkIngressEnabled bool // StatsPollingInterval determines how often metrics should be polled StatsPollingInterval time.Duration @@ -165,6 +167,7 @@ func NewConfig() (*Config, error) { EventStreamUseFentry: getEventStreamFentryValue(), EnvsWithValue: getStringSlice("envs_with_value"), NetworkEnabled: getBool("network.enabled"), + NetworkIngressEnabled: getBool("network.ingress.enabled"), StatsPollingInterval: time.Duration(getInt("events_stats.polling_interval")) * time.Second, SyscallsMonitorEnabled: getBool("syscalls_monitor.enabled"), diff --git a/pkg/security/probe/field_handlers_ebpf.go b/pkg/security/probe/field_handlers_ebpf.go index 8a4d07610c643c..ef10cd8193c670 100644 --- a/pkg/security/probe/field_handlers_ebpf.go +++ b/pkg/security/probe/field_handlers_ebpf.go @@ -541,3 +541,8 @@ func (fh *EBPFFieldHandlers) ResolveProcessCmdArgv(ev *model.Event, process *mod cmdline := []string{fh.ResolveProcessArgv0(ev, process)} return append(cmdline, fh.ResolveProcessArgv(ev, process)...) } + +// ResolveAWSSecurityCredentials resolves and updates the AWS security credentials of the input process entry +func (fh *EBPFFieldHandlers) ResolveAWSSecurityCredentials(e *model.Event) []model.AWSSecurityCredentials { + return fh.resolvers.ProcessResolver.FetchAWSSecurityCredentials(e) +} diff --git a/pkg/security/probe/field_handlers_ebpfless.go b/pkg/security/probe/field_handlers_ebpfless.go index 97462455369d52..72a86e3cafbd74 100644 --- a/pkg/security/probe/field_handlers_ebpfless.go +++ b/pkg/security/probe/field_handlers_ebpfless.go @@ -349,3 +349,8 @@ func (fh *EBPFLessFieldHandlers) ResolveProcessCmdArgv(ev *model.Event, process cmdline := []string{fh.ResolveProcessArgv0(ev, process)} return append(cmdline, fh.ResolveProcessArgv(ev, process)...) } + +// ResolveAWSSecurityCredentials resolves and updates the AWS security credentials of the input process entry +func (fh *EBPFLessFieldHandlers) ResolveAWSSecurityCredentials(_ *model.Event) []model.AWSSecurityCredentials { + return nil +} diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index f919d08bf2c322..53e2f88eff770e 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -468,7 +468,7 @@ func (p *EBPFProbe) unmarshalContexts(data []byte, event *model.Event) (int, err } func eventWithNoProcessContext(eventType model.EventType) bool { - return eventType == model.DNSEventType || eventType == model.LoadModuleEventType || eventType == model.UnloadModuleEventType + return eventType == model.DNSEventType || eventType == model.IMDSEventType || eventType == model.LoadModuleEventType || eventType == model.UnloadModuleEventType } func (p *EBPFProbe) unmarshalProcessCacheEntry(ev *model.Event, data []byte) (int, error) { @@ -922,6 +922,12 @@ func (p *EBPFProbe) handleEvent(CPU int, data []byte) { return } + case model.IMDSEventType: + if _, err = event.IMDS.UnmarshalBinary(data[offset:]); err != nil { + seclog.Errorf("failed to decode IMDS event: %s (offset %d, len %d)", err, offset, len(data)) + return + } + defer p.Resolvers.ProcessResolver.UpdateAWSSecurityCredentials(event.PIDContext.Pid, event) case model.BindEventType: if _, err = event.Bind.UnmarshalBinary(data[offset:]); err != nil { seclog.Errorf("failed to decode bind event: %s (offset %d, len %d)", err, offset, len(data)) @@ -1647,6 +1653,10 @@ func NewEBPFProbe(probe *Probe, config *config.Config, opts Opts, wmeta optional Name: "monitor_syscalls_map_enabled", Value: utils.BoolTouint64(probe.Opts.SyscallsMonitorEnabled), }, + manager.ConstantEditor{ + Name: "imds_ip", + Value: uint64(config.RuntimeSecurity.IMDSIPv4), + }, ) p.managerOptions.ConstantEditors = append(p.managerOptions.ConstantEditors, DiscarderConstants...) diff --git a/pkg/security/resolvers/process/resolver_ebpf.go b/pkg/security/resolvers/process/resolver_ebpf.go index d8d7c545ab262c..c08612b4d1fc62 100644 --- a/pkg/security/resolvers/process/resolver_ebpf.go +++ b/pkg/security/resolvers/process/resolver_ebpf.go @@ -1062,6 +1062,53 @@ func (p *EBPFResolver) UpdateCapset(pid uint32, e *model.Event) { } } +// UpdateAWSSecurityCredentials updates the list of AWS Security Credentials +func (p *EBPFResolver) UpdateAWSSecurityCredentials(pid uint32, e *model.Event) { + if len(e.IMDS.AWS.SecurityCredentials.AccessKeyID) == 0 { + return + } + + p.Lock() + defer p.Unlock() + + entry := p.entryCache[pid] + if entry != nil { + // check if this key is already in cache + for _, key := range entry.AWSSecurityCredentials { + if key.AccessKeyID == e.IMDS.AWS.SecurityCredentials.AccessKeyID { + return + } + } + entry.AWSSecurityCredentials = append(entry.AWSSecurityCredentials, e.IMDS.AWS.SecurityCredentials) + } +} + +// FetchAWSSecurityCredentials returns the list of AWS Security Credentials valid at the time of the event, and prunes +// expired entries +func (p *EBPFResolver) FetchAWSSecurityCredentials(e *model.Event) []model.AWSSecurityCredentials { + p.Lock() + defer p.Unlock() + + entry := p.entryCache[e.ProcessContext.Pid] + if entry != nil { + // check if we should delete + var toDelete []int + for id, key := range entry.AWSSecurityCredentials { + if key.Expiration.Before(e.ResolveEventTime()) { + toDelete = append([]int{id}, toDelete...) + } + } + + // delete expired entries + for _, id := range toDelete { + entry.AWSSecurityCredentials = append(entry.AWSSecurityCredentials[0:id], entry.AWSSecurityCredentials[id+1:]...) + } + + return entry.AWSSecurityCredentials + } + return nil +} + // Start starts the resolver func (p *EBPFResolver) Start(ctx context.Context) error { var err error diff --git a/pkg/security/resolvers/tc/resolver.go b/pkg/security/resolvers/tc/resolver.go index 175aeed4f74d1c..6b4c31921f1735 100644 --- a/pkg/security/resolvers/tc/resolver.go +++ b/pkg/security/resolvers/tc/resolver.go @@ -87,7 +87,7 @@ func (tcr *Resolver) SetupNewTCClassifierWithNetNSHandle(device model.NetDevice, defer tcr.Unlock() var combinedErr multierror.Error - for _, tcProbe := range probes.GetTCProbes() { + for _, tcProbe := range probes.GetTCProbes(tcr.config.NetworkIngressEnabled) { // make sure we're not overriding an existing network probe deviceKey := NetDeviceKey{IfIndex: device.IfIndex, NetNS: device.NetNS, NetworkDirection: tcProbe.NetworkDirection} _, ok := tcr.programs[deviceKey] diff --git a/pkg/security/secl/model/accessors_unix.go b/pkg/security/secl/model/accessors_unix.go index 54ddeb80487b48..a2b09d721158d4 100644 --- a/pkg/security/secl/model/accessors_unix.go +++ b/pkg/security/secl/model/accessors_unix.go @@ -36,6 +36,7 @@ func (m *Model) GetEventTypes() []eval.EventType { eval.EventType("dns"), eval.EventType("exec"), eval.EventType("exit"), + eval.EventType("imds"), eval.EventType("link"), eval.EventType("load_module"), eval.EventType("mkdir"), @@ -2569,6 +2570,78 @@ func (m *Model) GetEvaluator(field eval.Field, regID eval.RegisterID) (eval.Eval Field: field, Weight: eval.HandlerWeight, }, nil + case "imds.aws.is_imds_v2": + return &eval.BoolEvaluator{ + EvalFnc: func(ctx *eval.Context) bool { + ev := ctx.Event.(*Event) + return ev.IMDS.AWS.IsIMDSv2 + }, + Field: field, + Weight: eval.FunctionWeight, + }, nil + case "imds.aws.security_credentials.type": + return &eval.StringEvaluator{ + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.IMDS.AWS.SecurityCredentials.Type + }, + Field: field, + Weight: eval.FunctionWeight, + }, nil + case "imds.cloud_provider": + return &eval.StringEvaluator{ + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.IMDS.CloudProvider + }, + Field: field, + Weight: eval.FunctionWeight, + }, nil + case "imds.host": + return &eval.StringEvaluator{ + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.IMDS.Host + }, + Field: field, + Weight: eval.FunctionWeight, + }, nil + case "imds.server": + return &eval.StringEvaluator{ + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.IMDS.Server + }, + Field: field, + Weight: eval.FunctionWeight, + }, nil + case "imds.type": + return &eval.StringEvaluator{ + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.IMDS.Type + }, + Field: field, + Weight: eval.FunctionWeight, + }, nil + case "imds.url": + return &eval.StringEvaluator{ + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.IMDS.URL + }, + Field: field, + Weight: eval.FunctionWeight, + }, nil + case "imds.user_agent": + return &eval.StringEvaluator{ + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.IMDS.UserAgent + }, + Field: field, + Weight: eval.FunctionWeight, + }, nil case "link.file.change_time": return &eval.IntEvaluator{ EvalFnc: func(ctx *eval.Context) int { @@ -16597,6 +16670,14 @@ func (ev *Event) GetFields() []eval.Field { "exit.user_session.k8s_groups", "exit.user_session.k8s_uid", "exit.user_session.k8s_username", + "imds.aws.is_imds_v2", + "imds.aws.security_credentials.type", + "imds.cloud_provider", + "imds.host", + "imds.server", + "imds.type", + "imds.url", + "imds.user_agent", "link.file.change_time", "link.file.destination.change_time", "link.file.destination.filesystem", @@ -18348,6 +18429,22 @@ func (ev *Event) GetFieldValue(field eval.Field) (interface{}, error) { return ev.FieldHandlers.ResolveK8SUID(ev, &ev.Exit.Process.UserSession), nil case "exit.user_session.k8s_username": return ev.FieldHandlers.ResolveK8SUsername(ev, &ev.Exit.Process.UserSession), nil + case "imds.aws.is_imds_v2": + return ev.IMDS.AWS.IsIMDSv2, nil + case "imds.aws.security_credentials.type": + return ev.IMDS.AWS.SecurityCredentials.Type, nil + case "imds.cloud_provider": + return ev.IMDS.CloudProvider, nil + case "imds.host": + return ev.IMDS.Host, nil + case "imds.server": + return ev.IMDS.Server, nil + case "imds.type": + return ev.IMDS.Type, nil + case "imds.url": + return ev.IMDS.URL, nil + case "imds.user_agent": + return ev.IMDS.UserAgent, nil case "link.file.change_time": return int(ev.Link.Source.FileFields.CTime), nil case "link.file.destination.change_time": @@ -24442,6 +24539,22 @@ func (ev *Event) GetFieldEventType(field eval.Field) (eval.EventType, error) { return "exit", nil case "exit.user_session.k8s_username": return "exit", nil + case "imds.aws.is_imds_v2": + return "imds", nil + case "imds.aws.security_credentials.type": + return "imds", nil + case "imds.cloud_provider": + return "imds", nil + case "imds.host": + return "imds", nil + case "imds.server": + return "imds", nil + case "imds.type": + return "imds", nil + case "imds.url": + return "imds", nil + case "imds.user_agent": + return "imds", nil case "link.file.change_time": return "link", nil case "link.file.destination.change_time": @@ -26999,6 +27112,22 @@ func (ev *Event) GetFieldType(field eval.Field) (reflect.Kind, error) { return reflect.String, nil case "exit.user_session.k8s_username": return reflect.String, nil + case "imds.aws.is_imds_v2": + return reflect.Bool, nil + case "imds.aws.security_credentials.type": + return reflect.String, nil + case "imds.cloud_provider": + return reflect.String, nil + case "imds.host": + return reflect.String, nil + case "imds.server": + return reflect.String, nil + case "imds.type": + return reflect.String, nil + case "imds.url": + return reflect.String, nil + case "imds.user_agent": + return reflect.String, nil case "link.file.change_time": return reflect.Int, nil case "link.file.destination.change_time": @@ -31260,6 +31389,62 @@ func (ev *Event) SetFieldValue(field eval.Field, value interface{}) error { } ev.Exit.Process.UserSession.K8SUsername = rv return nil + case "imds.aws.is_imds_v2": + rv, ok := value.(bool) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "IMDS.AWS.IsIMDSv2"} + } + ev.IMDS.AWS.IsIMDSv2 = rv + return nil + case "imds.aws.security_credentials.type": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "IMDS.AWS.SecurityCredentials.Type"} + } + ev.IMDS.AWS.SecurityCredentials.Type = rv + return nil + case "imds.cloud_provider": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "IMDS.CloudProvider"} + } + ev.IMDS.CloudProvider = rv + return nil + case "imds.host": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "IMDS.Host"} + } + ev.IMDS.Host = rv + return nil + case "imds.server": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "IMDS.Server"} + } + ev.IMDS.Server = rv + return nil + case "imds.type": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "IMDS.Type"} + } + ev.IMDS.Type = rv + return nil + case "imds.url": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "IMDS.URL"} + } + ev.IMDS.URL = rv + return nil + case "imds.user_agent": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "IMDS.UserAgent"} + } + ev.IMDS.UserAgent = rv + return nil case "link.file.change_time": rv, ok := value.(int) if !ok { diff --git a/pkg/security/secl/model/category.go b/pkg/security/secl/model/category.go index 05b55919cae40d..125244c85d9361 100644 --- a/pkg/security/secl/model/category.go +++ b/pkg/security/secl/model/category.go @@ -43,7 +43,7 @@ func GetEventTypeCategory(eventType eval.EventType) EventCategory { case "bpf", "selinux", "mmap", "mprotect", "ptrace", "load_module", "unload_module", "bind": // TODO(will): "bind" is in this category because answering "NetworkCategory" would insert a network section in the serializer. return KernelCategory - case "dns": + case "dns", "imds": return NetworkCategory } diff --git a/pkg/security/secl/model/consts_common.go b/pkg/security/secl/model/consts_common.go index b521add0ef41ae..40bf707b52a8f0 100644 --- a/pkg/security/secl/model/consts_common.go +++ b/pkg/security/secl/model/consts_common.go @@ -69,6 +69,23 @@ const ( EventFlagsHasActiveActivityDump ) +const ( + // IMDSRequestType is used to specify that the event is an IDMS request event + IMDSRequestType = "request" + // IMDSResponseType is used to specify that the event is an IMDS response event + IMDSResponseType = "response" + // IMDSAWSCloudProvider is used to report that the IMDS event is for AWS + IMDSAWSCloudProvider = "aws" + // IMDSGCPCloudProvider is used to report that the IMDS event is for GCP + IMDSGCPCloudProvider = "gcp" + // IMDSAzureCloudProvider is used to report that the IMDS event is for Azure + IMDSAzureCloudProvider = "azure" + // IMDSIBMCloudProvider is used to report that the IMDS event is for ibm + IMDSIBMCloudProvider = "ibm" + // IMDSOracleCloudProvider is used to report that the IMDS event is for Oracle + IMDSOracleCloudProvider = "oracle" +) + var ( // vmConstants is the list of protection flags for a virtual memory segment // generate_constants:Virtual Memory flags,Virtual Memory flags define the protection of a virtual memory segment. diff --git a/pkg/security/secl/model/events.go b/pkg/security/secl/model/events.go index 32db5d574587ac..2e28d487f7ca5c 100644 --- a/pkg/security/secl/model/events.go +++ b/pkg/security/secl/model/events.go @@ -89,6 +89,8 @@ const ( UnshareMountNsEventType // SyscallsEventType Syscalls event SyscallsEventType + // IMDSEventType is sent when an IMDS request or qnswer is captured + IMDSEventType // MaxKernelEventType is used internally to get the maximum number of kernel events. MaxKernelEventType @@ -221,6 +223,8 @@ func (t EventType) String() string { return "unshare_mntns" case SyscallsEventType: return "syscalls" + case IMDSEventType: + return "imds" case CustomLostReadEventType: return "lost_events_read" diff --git a/pkg/security/secl/model/field_accessors_unix.go b/pkg/security/secl/model/field_accessors_unix.go index bfb5b197e0ebdd..d79832a5c4e1d9 100644 --- a/pkg/security/secl/model/field_accessors_unix.go +++ b/pkg/security/secl/model/field_accessors_unix.go @@ -2805,6 +2805,70 @@ func (ev *Event) GetExitUserSessionK8sUsername() string { return ev.FieldHandlers.ResolveK8SUsername(ev, &ev.Exit.Process.UserSession) } +// GetImdsAwsIsImdsV2 returns the value of the field, resolving if necessary +func (ev *Event) GetImdsAwsIsImdsV2() bool { + if ev.GetEventType().String() != "imds" { + return false + } + return ev.IMDS.AWS.IsIMDSv2 +} + +// GetImdsAwsSecurityCredentialsType returns the value of the field, resolving if necessary +func (ev *Event) GetImdsAwsSecurityCredentialsType() string { + if ev.GetEventType().String() != "imds" { + return "" + } + return ev.IMDS.AWS.SecurityCredentials.Type +} + +// GetImdsCloudProvider returns the value of the field, resolving if necessary +func (ev *Event) GetImdsCloudProvider() string { + if ev.GetEventType().String() != "imds" { + return "" + } + return ev.IMDS.CloudProvider +} + +// GetImdsHost returns the value of the field, resolving if necessary +func (ev *Event) GetImdsHost() string { + if ev.GetEventType().String() != "imds" { + return "" + } + return ev.IMDS.Host +} + +// GetImdsServer returns the value of the field, resolving if necessary +func (ev *Event) GetImdsServer() string { + if ev.GetEventType().String() != "imds" { + return "" + } + return ev.IMDS.Server +} + +// GetImdsType returns the value of the field, resolving if necessary +func (ev *Event) GetImdsType() string { + if ev.GetEventType().String() != "imds" { + return "" + } + return ev.IMDS.Type +} + +// GetImdsUrl returns the value of the field, resolving if necessary +func (ev *Event) GetImdsUrl() string { + if ev.GetEventType().String() != "imds" { + return "" + } + return ev.IMDS.URL +} + +// GetImdsUserAgent returns the value of the field, resolving if necessary +func (ev *Event) GetImdsUserAgent() string { + if ev.GetEventType().String() != "imds" { + return "" + } + return ev.IMDS.UserAgent +} + // GetLinkFileChangeTime returns the value of the field, resolving if necessary func (ev *Event) GetLinkFileChangeTime() uint64 { if ev.GetEventType().String() != "link" { diff --git a/pkg/security/secl/model/field_handlers_unix.go b/pkg/security/secl/model/field_handlers_unix.go index 4e04bf30846375..664eca8b809e83 100644 --- a/pkg/security/secl/model/field_handlers_unix.go +++ b/pkg/security/secl/model/field_handlers_unix.go @@ -403,6 +403,7 @@ func (ev *Event) resolveFields(forADs bool) { _ = ev.FieldHandlers.ResolveProcessEnvs(ev, ev.Exit.Process) _ = ev.FieldHandlers.ResolveProcessEnvp(ev, ev.Exit.Process) _ = ev.FieldHandlers.ResolveProcessEnvsTruncated(ev, ev.Exit.Process) + case "imds": case "link": _ = ev.FieldHandlers.ResolveFileFieldsUser(ev, &ev.Link.Source.FileFields) _ = ev.FieldHandlers.ResolveFileFieldsGroup(ev, &ev.Link.Source.FileFields) diff --git a/pkg/security/secl/model/model.go b/pkg/security/secl/model/model.go index 615cba0e3d6990..e8eb0e2ce5f01f 100644 --- a/pkg/security/secl/model/model.go +++ b/pkg/security/secl/model/model.go @@ -539,6 +539,36 @@ func (de *DNSEvent) Matches(new *DNSEvent) bool { return de.Name == new.Name && de.Type == new.Type && de.Class == new.Class } +// IMDSEvent represents an IMDS event +type IMDSEvent struct { + Type string `field:"type"` // SECLDoc[type] Definition:`the type of IMDS event` + CloudProvider string `field:"cloud_provider"` // SECLDoc[cloud_provider] Definition:`the intended cloud provider of the IMDS event` + URL string `field:"url"` // SECLDoc[url] Definition:`the queried IMDS URL` + Host string `field:"host"` // SECLDoc[host] Definition:`the host of the HTTP protocol` + UserAgent string `field:"user_agent"` // SECLDoc[user_agent] Definition:`the user agent of the HTTP client` + Server string `field:"server"` // SECLDoc[server] Definition:`the server header of a response` + + // The fields below are optional and cloud specific fields + AWS AWSIMDSEvent `field:"aws"` // SECLDoc[aws] Definition:`the AWS specific data parsed from the IMDS event` +} + +// AWSIMDSEvent holds data from an AWS IMDS event +type AWSIMDSEvent struct { + IsIMDSv2 bool `field:"is_imds_v2"` // SECLDoc[is_imds_v2] Definition:`a boolean which specifies if the IMDS event follows IMDSv1 or IMDSv2 conventions` + SecurityCredentials AWSSecurityCredentials `field:"security_credentials"` // SECLDoc[credentials] Definition:`the security credentials in the IMDS answer` +} + +// AWSSecurityCredentials is used to parse the fields that are none to be free of credentials or secrets +type AWSSecurityCredentials struct { + Code string `field:"-" json:"Code"` + Type string `field:"type" json:"Type"` // SECLDoc[type] Definition:`the security credentials type` + AccessKeyID string `field:"-" json:"AccessKeyId"` + LastUpdated string `field:"-" json:"LastUpdated"` + Expiration time.Time `field:"-"` + + ExpirationRaw string `field:"-" json:"Expiration"` +} + // BaseExtraFieldHandlers handlers not hold by any field type BaseExtraFieldHandlers interface { ResolveProcessCacheEntry(ev *Event) (*ProcessCacheEntry, bool) diff --git a/pkg/security/secl/model/model_helpers_unix.go b/pkg/security/secl/model/model_helpers_unix.go index 2e4d33552b8af9..47265c58b816f5 100644 --- a/pkg/security/secl/model/model_helpers_unix.go +++ b/pkg/security/secl/model/model_helpers_unix.go @@ -400,6 +400,11 @@ func (dfh *FakeFieldHandlers) ResolveHashes(_ EventType, _ *Process, _ *FileEven // ResolveUserSessionContext resolves and updates the provided user session context func (dfh *FakeFieldHandlers) ResolveUserSessionContext(_ *UserSessionContext) {} +// ResolveAWSSecurityCredentials resolves and updates the AWS security credentials of the input process entry +func (dfh *FakeFieldHandlers) ResolveAWSSecurityCredentials(_ *Event) []AWSSecurityCredentials { + return nil +} + // SELinuxEventKind represents the event kind for SELinux events type SELinuxEventKind uint32 @@ -417,4 +422,5 @@ type ExtraFieldHandlers interface { BaseExtraFieldHandlers ResolveHashes(eventType EventType, process *Process, file *FileEvent) []string ResolveUserSessionContext(evtCtx *UserSessionContext) + ResolveAWSSecurityCredentials(event *Event) []AWSSecurityCredentials } diff --git a/pkg/security/secl/model/model_unix.go b/pkg/security/secl/model/model_unix.go index 3b2278f8492703..e888040f225570 100644 --- a/pkg/security/secl/model/model_unix.go +++ b/pkg/security/secl/model/model_unix.go @@ -67,6 +67,7 @@ type Event struct { // network events DNS DNSEvent `field:"dns" event:"dns"` // [7.36] [Network] A DNS request was sent + IMDS IMDSEvent `field:"imds" event:"imds"` // [7.55] [Network] An IMDS event was captured Bind BindEvent `field:"bind" event:"bind"` // [7.37] [Network] A bind was executed // internal usage @@ -187,6 +188,8 @@ type Process struct { UserSession UserSessionContext `field:"user_session"` // SECLDoc[user_session] Definition:`User Session context of this process` + AWSSecurityCredentials []AWSSecurityCredentials `field:"-"` + ArgsID uint32 `field:"-"` EnvsID uint32 `field:"-"` diff --git a/pkg/security/secl/model/unmarshallers_linux.go b/pkg/security/secl/model/unmarshallers_linux.go index e3dbc9f72a130d..c5836fea545191 100644 --- a/pkg/security/secl/model/unmarshallers_linux.go +++ b/pkg/security/secl/model/unmarshallers_linux.go @@ -7,9 +7,15 @@ package model import ( + "bufio" + "bytes" "encoding/binary" + "encoding/json" "fmt" + "io" "math/bits" + "net/http" + "slices" "strings" "time" @@ -1023,6 +1029,93 @@ func (e *DNSEvent) UnmarshalBinary(data []byte) (int, error) { return len(data), nil } +// UnmarshalBinary unmarshalls a binary representation of itself +func (e *IMDSEvent) UnmarshalBinary(data []byte) (int, error) { + if len(data) < 10 { + return 0, ErrNotEnoughData + } + + firstWord := strings.SplitN(string(data[0:10]), " ", 2) + switch { + case strings.HasPrefix(firstWord[0], "HTTP"): + // this is an IMDS response + e.Type = IMDSResponseType + resp, err := http.ReadResponse(bufio.NewReader(bytes.NewBuffer(data)), nil) + if err != nil { + return 0, fmt.Errorf("failed to parse IMDS response: %v", err) + } + e.fillFromIMDSHeader(resp.Header, "") + + // try to parse cloud provider specific data + if e.CloudProvider == IMDSAWSCloudProvider { + b := new(bytes.Buffer) + _, err = io.Copy(b, resp.Body) + if err == nil { + _ = resp.Body.Close() + // we don't care about errors, this unmarshalling will only work for token responses + _ = e.AWS.SecurityCredentials.UnmarshalBinary(b.Bytes()) + if len(e.AWS.SecurityCredentials.ExpirationRaw) > 0 { + e.AWS.SecurityCredentials.Expiration, _ = time.Parse(time.RFC3339, e.AWS.SecurityCredentials.ExpirationRaw) + } + } + } + case slices.Contains([]string{ + http.MethodGet, + http.MethodHead, + http.MethodPost, + http.MethodPut, + http.MethodPatch, + http.MethodDelete, + http.MethodConnect, + http.MethodOptions, + http.MethodTrace, + }, firstWord[0]): + // this is an IMDS request + e.Type = IMDSRequestType + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBuffer(data))) + if err != nil { + return 0, fmt.Errorf("failed to parse IMDS request: %v", err) + } + e.URL = req.URL.String() + e.fillFromIMDSHeader(req.Header, e.URL) + e.Host = req.Host + e.UserAgent = req.UserAgent() + default: + return 0, fmt.Errorf("invalid HTTP packet: unknown first word %s", firstWord[0]) + } + + return len(data), nil +} + +func (e *IMDSEvent) fillFromIMDSHeader(header http.Header, url string) { + if header != nil { + e.Server = header.Get("Server") + + // guess the cloud provider from headers and the URL (this is a best effort resolution since some cloud provider + // don't require any particular headers). + if flavor := header.Get("Metadata-Flavor"); flavor == "Google" { + e.CloudProvider = IMDSGCPCloudProvider + } else if flavor == "ibm" { + e.CloudProvider = IMDSIBMCloudProvider + } else if authorization := header.Get("Authorization"); authorization == "Bearer Oracle" || strings.HasPrefix(url, "/opc") { + e.CloudProvider = IMDSOracleCloudProvider + } else if metadata := header.Get("Metadata"); metadata == "true" { + e.CloudProvider = IMDSAzureCloudProvider + } else { + e.CloudProvider = IMDSAWSCloudProvider + + // check if this is an IMDSv2 request + e.AWS.IsIMDSv2 = len(header.Get("x-aws-ec2-metadata-token-ttl-seconds")) > 0 || + len(header.Get("x-aws-ec2-metadata-token")) > 0 + } + } +} + +// UnmarshalBinary extract scrubbed data from an AWS IMDS security credentials response body +func (creds *AWSSecurityCredentials) UnmarshalBinary(body []byte) error { + return json.Unmarshal(body, creds) +} + // UnmarshalBinary unmarshalls a binary representation of itself func (d *NetDevice) UnmarshalBinary(data []byte) (int, error) { if len(data[:]) < 32 { diff --git a/pkg/security/security_profile/activity_tree/activity_tree.go b/pkg/security/security_profile/activity_tree/activity_tree.go index 562dbbe27ba14a..98202ae296f96b 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree.go +++ b/pkg/security/security_profile/activity_tree/activity_tree.go @@ -272,6 +272,15 @@ func (at *ActivityTree) isEventValid(event *model.Event, dryRun bool) (bool, err } return false, errors.New("invalid event: invalid bind family") } + case model.IMDSEventType: + // ignore IMDS answers without AccessKeyIDS + if event.IMDS.Type == model.IMDSResponseType && len(event.IMDS.AWS.SecurityCredentials.AccessKeyID) == 0 { + return false, fmt.Errorf("untraced event: IMDS response without credentials") + } + // ignore IMDS requests without URLs + if event.IMDS.Type == model.IMDSRequestType && len(event.IMDS.URL) == 0 { + return false, fmt.Errorf("invalid event: IMDS request without any URL") + } } return true, nil } @@ -340,6 +349,8 @@ func (at *ActivityTree) insertEvent(event *model.Event, dryRun bool, insertMissi return node.InsertFileEvent(&event.Open.File, event, imageTag, generationType, at.Stats, dryRun, at.pathsReducer, resolvers), nil case model.DNSEventType: return node.InsertDNSEvent(event, imageTag, generationType, at.Stats, at.DNSNames, dryRun, at.DNSMatchMaxDepth), nil + case model.IMDSEventType: + return node.InsertIMDSEvent(event, imageTag, generationType, at.Stats, dryRun), nil case model.BindEventType: return node.InsertBindEvent(event, imageTag, generationType, at.Stats, dryRun), nil case model.SyscallsEventType: diff --git a/pkg/security/security_profile/activity_tree/activity_tree_graph.go b/pkg/security/security_profile/activity_tree/activity_tree_graph.go index c2be7b3bc5709a..4ac254f015276b 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_graph.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_graph.go @@ -102,6 +102,17 @@ func (at *ActivityTree) prepareProcessNode(p *ProcessNode, data *utils.Graph, re } } + for _, n := range p.IMDSEvents { + imdsNodeID, ok := at.prepareIMDSNode(n, data, panGraphID) + if ok { + data.Edges = append(data.Edges, &utils.Edge{ + From: panGraphID, + To: imdsNodeID, + Color: networkColor, + }) + } + } + for _, f := range p.Files { fileID := at.prepareFileNode(f, data, "", panGraphID) data.Edges = append(data.Edges, &utils.Edge{ @@ -160,6 +171,48 @@ func (at *ActivityTree) prepareDNSNode(n *DNSNode, data *utils.Graph, processID return dnsNode.ID, true } +func (at *ActivityTree) prepareIMDSNode(n *IMDSNode, data *utils.Graph, processID utils.GraphID) (utils.GraphID, bool) { + label := "<" + label += "" + label += "" + if len(n.Event.UserAgent) > 0 { + label += "" + } + if len(n.Event.UserAgent) > 0 { + label += "" + } + if len(n.Event.Server) > 0 { + label += "" + } + if len(n.Event.Host) > 0 { + label += "" + } + if n.Event.CloudProvider == model.IMDSAWSCloudProvider { + label += "" + if len(n.Event.AWS.SecurityCredentials.AccessKeyID) > 0 { + label += "" + } + } + label += "
IMDS" + n.Event.Type + "
Cloud provider" + n.Event.CloudProvider + "
URL" + n.Event.URL + "
User agent" + n.Event.UserAgent + "
Server" + n.Event.Server + "
Host" + n.Event.Host + "
IMDSv2" + fmt.Sprintf("%v", n.Event.AWS.IsIMDSv2) + "
AccessKeyID " + n.Event.AWS.SecurityCredentials.AccessKeyID + "
>" + + imdsNode := &utils.Node{ + ID: processID.Derive(utils.NewNodeIDFromPtr(n)), + Label: label, + Size: 30, + Color: networkColor, + Shape: networkShape, + IsTable: true, + } + switch n.GenerationType { + case Runtime, Snapshot, Unknown: + imdsNode.FillColor = networkRuntimeColor + case ProfileDrift: + imdsNode.FillColor = networkProfileDriftColor + } + data.Nodes[imdsNode.ID] = imdsNode + return imdsNode.ID, true +} + func (at *ActivityTree) prepareSocketNode(n *SocketNode, data *utils.Graph, processID utils.GraphID) utils.GraphID { targetID := processID.Derive(utils.NewNodeIDFromPtr(n)) @@ -238,7 +291,7 @@ func (at *ActivityTree) prepareFileNode(f *FileNode, data *utils.Graph, prefix s } func (at *ActivityTree) prepareSyscallsNode(p *ProcessNode, data *utils.Graph) utils.GraphID { - label := "<" + label := "<
" for _, s := range p.Syscalls { label += "" } diff --git a/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go b/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go index c19e327fe579ce..e4140c253144e7 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_proto_dec_v1.go @@ -36,6 +36,7 @@ func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.Proce Children: make([]*ProcessNode, 0, len(pan.Children)), Files: make(map[string]*FileNode, len(pan.Files)), DNSNames: make(map[string]*DNSNode, len(pan.DnsNames)), + IMDSEvents: make(map[model.IMDSEvent]*IMDSNode, len(pan.ImdsEvents)), Sockets: make([]*SocketNode, 0, len(pan.Sockets)), Syscalls: make([]int, 0, len(pan.Syscalls)), ImageTags: pan.ImageTags, @@ -62,6 +63,11 @@ func protoDecodeProcessActivityNode(parent ProcessNodeParent, pan *adproto.Proce } } + for _, imds := range pan.ImdsEvents { + node := protoDecodeIMDSNode(imds) + ppan.IMDSEvents[node.Event] = node + } + for _, socket := range pan.Sockets { ppan.Sockets = append(ppan.Sockets, protoDecodeProtoSocket(socket)) } @@ -236,6 +242,24 @@ func protoDecodeDNSNode(dn *adproto.DNSNode) *DNSNode { return pdn } +func protoDecodeIMDSNode(in *adproto.IMDSNode) *IMDSNode { + if in == nil { + return nil + } + + node := &IMDSNode{ + MatchedRules: make([]*model.MatchedRule, 0, len(in.MatchedRules)), + ImageTags: in.ImageTags, + Event: protoDecodeIMDSEvent(in.Event), + } + + for _, rule := range in.MatchedRules { + node.MatchedRules = append(node.MatchedRules, protoDecodeProtoMatchedRule(rule)) + } + + return node +} + func protoDecodeDNSInfo(ev *adproto.DNSInfo) model.DNSEvent { if ev == nil { return model.DNSEvent{} @@ -250,6 +274,50 @@ func protoDecodeDNSInfo(ev *adproto.DNSInfo) model.DNSEvent { } } +func protoDecodeIMDSEvent(ie *adproto.IMDSEvent) model.IMDSEvent { + if ie == nil { + return model.IMDSEvent{} + } + + return model.IMDSEvent{ + Type: ie.Type, + CloudProvider: ie.CloudProvider, + URL: ie.Url, + Host: ie.Host, + Server: ie.Server, + UserAgent: ie.UserAgent, + AWS: protoDecodeAWSIMDSEvent(ie.Aws), + } +} + +func protoDecodeAWSIMDSEvent(aie *adproto.AWSIMDSEvent) model.AWSIMDSEvent { + if aie == nil { + return model.AWSIMDSEvent{} + } + + return model.AWSIMDSEvent{ + IsIMDSv2: aie.IsImdsV2, + SecurityCredentials: protoDecodeAWSSecurityCredentials(aie.SecurityCredentials), + } +} + +func protoDecodeAWSSecurityCredentials(creds *adproto.AWSSecurityCredentials) model.AWSSecurityCredentials { + if creds == nil { + return model.AWSSecurityCredentials{} + } + + expiration, _ := time.Parse(time.RFC3339, creds.ExpirationRaw) + + return model.AWSSecurityCredentials{ + Code: creds.Code, + Type: creds.Type, + AccessKeyID: creds.AccessKeyId, + LastUpdated: creds.LastUpdated, + ExpirationRaw: creds.ExpirationRaw, + Expiration: expiration, + } +} + func protoDecodeProtoSocket(sn *adproto.SocketNode) *SocketNode { if sn == nil { return nil diff --git a/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go b/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go index a18caa2e3e850f..bc54cab5015e43 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_proto_enc_v1.go @@ -40,6 +40,7 @@ func processActivityNodeToProto(pan *ProcessNode) *adproto.ProcessActivityNode { Children: make([]*adproto.ProcessActivityNode, 0, len(pan.Children)), Files: make([]*adproto.FileActivityNode, 0, len(pan.Files)), DnsNames: make([]*adproto.DNSNode, 0, len(pan.DNSNames)), + ImdsEvents: make([]*adproto.IMDSNode, 0, len(pan.IMDSEvents)), Sockets: make([]*adproto.SocketNode, 0, len(pan.Sockets)), Syscalls: make([]uint32, 0, len(pan.Syscalls)), ImageTags: pan.ImageTags, @@ -61,6 +62,10 @@ func processActivityNodeToProto(pan *ProcessNode) *adproto.ProcessActivityNode { ppan.DnsNames = append(ppan.DnsNames, dnsNodeToProto(dns)) } + for _, imds := range pan.IMDSEvents { + ppan.ImdsEvents = append(ppan.ImdsEvents, imdsNodeToProto(imds)) + } + for _, socket := range pan.Sockets { ppan.Sockets = append(ppan.Sockets, socketNodeToProto(socket)) } @@ -243,6 +248,48 @@ func dnsEventToProto(ev *model.DNSEvent) *adproto.DNSInfo { } } +func imdsNodeToProto(in *IMDSNode) *adproto.IMDSNode { + if in == nil { + return nil + } + + pin := &adproto.IMDSNode{ + MatchedRules: make([]*adproto.MatchedRule, 0, len(in.MatchedRules)), + ImageTags: in.ImageTags, + Event: imdsEventToProto(in.Event), + } + + return pin +} + +func imdsEventToProto(event model.IMDSEvent) *adproto.IMDSEvent { + return &adproto.IMDSEvent{ + Type: event.Type, + CloudProvider: event.CloudProvider, + Url: event.URL, + Host: event.Host, + UserAgent: event.UserAgent, + Server: event.Server, + Aws: awsIMDSEventToProto(event), + } +} + +func awsIMDSEventToProto(event model.IMDSEvent) *adproto.AWSIMDSEvent { + if event.CloudProvider != model.IMDSAWSCloudProvider { + return nil + } + return &adproto.AWSIMDSEvent{ + IsImdsV2: event.AWS.IsIMDSv2, + SecurityCredentials: &adproto.AWSSecurityCredentials{ + Type: event.AWS.SecurityCredentials.Type, + Code: event.AWS.SecurityCredentials.Code, + AccessKeyId: event.AWS.SecurityCredentials.AccessKeyID, + LastUpdated: event.AWS.SecurityCredentials.LastUpdated, + ExpirationRaw: event.AWS.SecurityCredentials.ExpirationRaw, + }, + } +} + func socketNodeToProto(sn *SocketNode) *adproto.SocketNode { if sn == nil { return nil diff --git a/pkg/security/security_profile/activity_tree/activity_tree_stats.go b/pkg/security/security_profile/activity_tree/activity_tree_stats.go index 5eabf809bc7a0d..0ed229e130a22c 100644 --- a/pkg/security/security_profile/activity_tree/activity_tree_stats.go +++ b/pkg/security/security_profile/activity_tree/activity_tree_stats.go @@ -25,6 +25,7 @@ type Stats struct { FileNodes int64 DNSNodes int64 SocketNodes int64 + IMDSNodes int64 counts map[model.EventType]*statsPerEventType } @@ -70,6 +71,7 @@ func (stats *Stats) ApproximateSize() int64 { total += stats.FileNodes * int64(unsafe.Sizeof(FileNode{})) // 80 total += stats.DNSNodes * int64(unsafe.Sizeof(DNSNode{})) // 24 total += stats.SocketNodes * int64(unsafe.Sizeof(SocketNode{})) // 40 + total += stats.IMDSNodes * int64(unsafe.Sizeof(IMDSNode{})) return total } diff --git a/pkg/security/security_profile/activity_tree/imds_node.go b/pkg/security/security_profile/activity_tree/imds_node.go new file mode 100644 index 00000000000000..5f787dd1631b29 --- /dev/null +++ b/pkg/security/security_profile/activity_tree/imds_node.go @@ -0,0 +1,50 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux + +// Package activitytree holds activitytree related files +package activitytree + +import ( + "github.com/DataDog/datadog-agent/pkg/security/secl/model" +) + +// IMDSNode is used to store a IMDS node +type IMDSNode struct { + MatchedRules []*model.MatchedRule + ImageTags []string + GenerationType NodeGenerationType + + Event model.IMDSEvent +} + +// NewIMDSNode returns a new IMDSNode instance +func NewIMDSNode(event *model.IMDSEvent, rules []*model.MatchedRule, generationType NodeGenerationType, imageTag string) *IMDSNode { + node := &IMDSNode{ + MatchedRules: rules, + GenerationType: generationType, + Event: *event, + } + if imageTag != "" { + node.ImageTags = []string{imageTag} + } + return node +} + +func (imds *IMDSNode) appendImageTag(imageTag string) { + imds.ImageTags, _ = AppendIfNotPresent(imds.ImageTags, imageTag) +} + +func (imds *IMDSNode) evictImageTag(imageTag string) bool { + imageTags, removed := removeImageTagFromList(imds.ImageTags, imageTag) + if removed { + if len(imageTags) == 0 { + return true + } + imds.ImageTags = imageTags + } + return false +} diff --git a/pkg/security/security_profile/activity_tree/process_node.go b/pkg/security/security_profile/activity_tree/process_node.go index 9d12ee062b47a4..450774141ee70e 100644 --- a/pkg/security/security_profile/activity_tree/process_node.go +++ b/pkg/security/security_profile/activity_tree/process_node.go @@ -38,8 +38,10 @@ type ProcessNode struct { ImageTags []string MatchedRules []*model.MatchedRule - Files map[string]*FileNode - DNSNames map[string]*DNSNode + Files map[string]*FileNode + DNSNames map[string]*DNSNode + IMDSEvents map[model.IMDSEvent]*IMDSNode + Sockets []*SocketNode Syscalls []int Children []*ProcessNode @@ -59,6 +61,7 @@ func NewProcessNode(entry *model.ProcessCacheEntry, generationType NodeGeneratio GenerationType: generationType, Files: make(map[string]*FileNode), DNSNames: make(map[string]*DNSNode), + IMDSEvents: make(map[model.IMDSEvent]*IMDSNode), } } @@ -134,6 +137,12 @@ func (pn *ProcessNode) debug(w io.Writer, prefix string) { fmt.Fprintf(w, "%s - %s\n", prefix, dnsName) } } + if len(pn.IMDSEvents) > 0 { + fmt.Fprintf(w, "%s imds:\n", prefix) + for evt := range pn.IMDSEvents { + fmt.Fprintf(w, "%s - %s | %s\n", prefix, evt.CloudProvider, evt.Type) + } + } if len(pn.Children) > 0 { fmt.Fprintf(w, "%s children:\n", prefix) for _, child := range pn.Children { @@ -304,6 +313,23 @@ func (pn *ProcessNode) InsertDNSEvent(evt *model.Event, imageTag string, generat return true } +// InsertIMDSEvent inserts an IMDS event in a process node +func (pn *ProcessNode) InsertIMDSEvent(evt *model.Event, imageTag string, generationType NodeGenerationType, stats *Stats, dryRun bool) bool { + imdsNode, ok := pn.IMDSEvents[evt.IMDS] + if ok { + imdsNode.MatchedRules = model.AppendMatchedRule(imdsNode.MatchedRules, evt.Rules) + imdsNode.appendImageTag(imageTag) + return false + } + + if !dryRun { + // create new node + pn.IMDSEvents[evt.IMDS] = NewIMDSNode(&evt.IMDS, evt.Rules, generationType, imageTag) + stats.IMDSNodes++ + } + return true +} + // InsertBindEvent inserts a bind event in a process node func (pn *ProcessNode) InsertBindEvent(evt *model.Event, imageTag string, generationType NodeGenerationType, stats *Stats, dryRun bool) bool { if evt.Bind.SyscallEvent.Retval != 0 { @@ -409,6 +435,13 @@ func (pn *ProcessNode) EvictImageTag(imageTag string, DNSNames *utils.StringKeys } } + // Evict image tag from IMDS nodes + for key, imds := range pn.IMDSEvents { + if shouldRemoveNode := imds.evictImageTag(imageTag); shouldRemoveNode { + delete(pn.IMDSEvents, key) + } + } + newSockets := []*SocketNode{} for _, sock := range pn.Sockets { if shouldRemoveNode := sock.evictImageTag(imageTag); !shouldRemoveNode { diff --git a/pkg/security/security_profile/dump/load_controller.go b/pkg/security/security_profile/dump/load_controller.go index 2f5a6f809050ea..b6899e2a668275 100644 --- a/pkg/security/security_profile/dump/load_controller.go +++ b/pkg/security/security_profile/dump/load_controller.go @@ -22,7 +22,7 @@ import ( var ( // TracedEventTypesReductionOrder is the order by which event types are reduced - TracedEventTypesReductionOrder = []model.EventType{model.BindEventType, model.DNSEventType, model.SyscallsEventType, model.FileOpenEventType} + TracedEventTypesReductionOrder = []model.EventType{model.BindEventType, model.IMDSEventType, model.DNSEventType, model.SyscallsEventType, model.FileOpenEventType} absoluteMinimumDumpTimeout = 10 * time.Second ) diff --git a/pkg/security/security_profile/dump/manager.go b/pkg/security/security_profile/dump/manager.go index d0a23f06620fd0..88b4514efe6b4e 100644 --- a/pkg/security/security_profile/dump/manager.go +++ b/pkg/security/security_profile/dump/manager.go @@ -738,11 +738,17 @@ func (pces *processCacheEntrySearcher) SearchTracedProcessCacheEntry(entry *mode imageTag := utils.GetTagValue("image_tag", pces.ad.Tags) for _, parent = range ancestors { - _, _, err := pces.ad.ActivityTree.CreateProcessNode(parent, imageTag, activity_tree.Snapshot, false, pces.adm.resolvers) + node, _, err := pces.ad.ActivityTree.CreateProcessNode(parent, imageTag, activity_tree.Snapshot, false, pces.adm.resolvers) if err != nil { // if one of the parents wasn't inserted, leave now break } + if node != nil { + // This step is important to populate the kernel space "traced_pids" map. Some traced event types use this + // map directly (as opposed to "traced_cgroups") to determine if their events should be tagged as dump + // samples. + pces.ad.updateTracedPid(node.Process.Pid) + } } } diff --git a/pkg/security/serializers/serializers_base.go b/pkg/security/serializers/serializers_base.go index 32842b8e6f16d9..6bafe842564d6a 100644 --- a/pkg/security/serializers/serializers_base.go +++ b/pkg/security/serializers/serializers_base.go @@ -113,6 +113,50 @@ type NetworkContextSerializer struct { Size uint32 `json:"size"` } +// AWSSecurityCredentialsSerializer serializes the security credentials from an AWS IMDS request +// easyjson:json +type AWSSecurityCredentialsSerializer struct { + // code is the IMDS server code response + Code string `json:"code"` + // type is the security credentials type + Type string `json:"type"` + // access_key_id is the unique access key ID of the credentials + AccessKeyID string `json:"access_key_id"` + // last_updated is the last time the credentials were updated + LastUpdated string `json:"last_updated"` + // expiration is the expiration date of the credentials + Expiration string `json:"expiration"` +} + +// AWSIMDSEventSerializer serializes an AWS IMDS event to JSON +// easyjson:json +type AWSIMDSEventSerializer struct { + // is_imds_v2 reports if the IMDS event follows IMDSv1 or IMDSv2 conventions + IsIMDSv2 bool `json:"is_imds_v2"` + // SecurityCredentials holds the scrubbed data collected on the security credentials + SecurityCredentials *AWSSecurityCredentialsSerializer `json:"security_credentials,omitempty"` +} + +// IMDSEventSerializer serializes an IMDS event to JSON +// easyjson:json +type IMDSEventSerializer struct { + // type is the type of IMDS event + Type string `json:"type"` + // cloud_provider is the intended cloud provider of the IMDS event + CloudProvider string `json:"cloud_provider"` + // url is the url of the IMDS request + URL string `json:"url,omitempty"` + // host is the host of the HTTP protocol + Host string `json:"host,omitempty"` + // user_agent is the user agent of the HTTP client + UserAgent string `json:"user_agent,omitempty"` + // server is the server header of a response + Server string `json:"server,omitempty"` + + // AWS holds the AWS specific data parsed from the IMDS event + AWS *AWSIMDSEventSerializer `json:"aws,omitempty"` +} + // DNSQuestionSerializer serializes a DNS question to JSON // easyjson:json type DNSQuestionSerializer struct { @@ -197,6 +241,40 @@ func newDNSEventSerializer(d *model.DNSEvent) *DNSEventSerializer { } } +// nolint: deadcode, unused +func newAWSSecurityCredentialsSerializer(creds *model.AWSSecurityCredentials) *AWSSecurityCredentialsSerializer { + return &AWSSecurityCredentialsSerializer{ + Code: creds.Code, + Type: creds.Type, + LastUpdated: creds.LastUpdated, + Expiration: creds.ExpirationRaw, + AccessKeyID: creds.AccessKeyID, + } +} + +// nolint: deadcode, unused +func newIMDSEventSerializer(e *model.IMDSEvent) *IMDSEventSerializer { + var aws *AWSIMDSEventSerializer + if e.CloudProvider == model.IMDSAWSCloudProvider { + aws = &AWSIMDSEventSerializer{ + IsIMDSv2: e.AWS.IsIMDSv2, + } + if len(e.AWS.SecurityCredentials.AccessKeyID) > 0 { + aws.SecurityCredentials = newAWSSecurityCredentialsSerializer(&e.AWS.SecurityCredentials) + } + } + + return &IMDSEventSerializer{ + Type: e.Type, + CloudProvider: e.CloudProvider, + URL: e.URL, + Host: e.Host, + UserAgent: e.UserAgent, + Server: e.Server, + AWS: aws, + } +} + // nolint: deadcode, unused func newIPPortSerializer(c *model.IPPortContext) IPPortSerializer { return IPPortSerializer{ diff --git a/pkg/security/serializers/serializers_base_linux_easyjson.go b/pkg/security/serializers/serializers_base_linux_easyjson.go index 230d4aceeb4b99..9612ce25cdcdaa 100644 --- a/pkg/security/serializers/serializers_base_linux_easyjson.go +++ b/pkg/security/serializers/serializers_base_linux_easyjson.go @@ -351,6 +351,37 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers1(i in.Delim(']') } } + case "aws_security_credentials": + if in.IsNull() { + in.Skip() + out.AWSSecurityCredentials = nil + } else { + in.Delim('[') + if out.AWSSecurityCredentials == nil { + if !in.IsDelim(']') { + out.AWSSecurityCredentials = make([]*AWSSecurityCredentialsSerializer, 0, 8) + } else { + out.AWSSecurityCredentials = []*AWSSecurityCredentialsSerializer{} + } + } else { + out.AWSSecurityCredentials = (out.AWSSecurityCredentials)[:0] + } + for !in.IsDelim(']') { + var v7 *AWSSecurityCredentialsSerializer + if in.IsNull() { + in.Skip() + v7 = nil + } else { + if v7 == nil { + v7 = new(AWSSecurityCredentialsSerializer) + } + (*v7).UnmarshalEasyJSON(in) + } + out.AWSSecurityCredentials = append(out.AWSSecurityCredentials, v7) + in.WantComma() + } + in.Delim(']') + } default: in.SkipRecursive() } @@ -381,14 +412,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers1(o } { out.RawByte('[') - for v7, v8 := range in.Ancestors { - if v7 > 0 { + for v8, v9 := range in.Ancestors { + if v8 > 0 { out.RawByte(',') } - if v8 == nil { + if v9 == nil { out.RawString("null") } else { - (*v8).MarshalEasyJSON(out) + (*v9).MarshalEasyJSON(out) } } out.RawByte(']') @@ -524,11 +555,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers1(o out.RawString(prefix) { out.RawByte('[') - for v9, v10 := range in.Args { - if v9 > 0 { + for v10, v11 := range in.Args { + if v10 > 0 { out.RawByte(',') } - out.String(string(v10)) + out.String(string(v11)) } out.RawByte(']') } @@ -543,11 +574,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers1(o out.RawString(prefix) { out.RawByte('[') - for v11, v12 := range in.Envs { - if v11 > 0 { + for v12, v13 := range in.Envs { + if v12 > 0 { out.RawByte(',') } - out.String(string(v12)) + out.String(string(v13)) } out.RawByte(']') } @@ -584,11 +615,29 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers1(o out.RawString("null") } else { out.RawByte('[') - for v13, v14 := range *in.Syscalls { - if v13 > 0 { + for v14, v15 := range *in.Syscalls { + if v14 > 0 { out.RawByte(',') } - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(out, v14) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers2(out, v15) + } + out.RawByte(']') + } + } + if len(in.AWSSecurityCredentials) != 0 { + const prefix string = ",\"aws_security_credentials\":" + out.RawString(prefix) + { + out.RawByte('[') + for v16, v17 := range in.AWSSecurityCredentials { + if v16 > 0 { + out.RawByte(',') + } + if v17 == nil { + out.RawString("null") + } else { + (*v17).MarshalEasyJSON(out) + } } out.RawByte(']') } @@ -794,9 +843,9 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(i out.Tags = (out.Tags)[:0] } for !in.IsDelim(']') { - var v15 string - v15 = string(in.String()) - out.Tags = append(out.Tags, v15) + var v18 string + v18 = string(in.String()) + out.Tags = append(out.Tags, v18) in.WantComma() } in.Delim(']') @@ -845,11 +894,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers4(o } { out.RawByte('[') - for v16, v17 := range in.Tags { - if v16 > 0 { + for v19, v20 := range in.Tags { + if v19 > 0 { out.RawByte(',') } - out.String(string(v17)) + out.String(string(v20)) } out.RawByte(']') } @@ -1011,7 +1060,109 @@ func (v IPPortFamilySerializer) MarshalEasyJSON(w *jwriter.Writer) { func (v *IPPortFamilySerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers6(l, v) } -func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(in *jlexer.Lexer, out *ExitEventSerializer) { +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(in *jlexer.Lexer, out *IMDSEventSerializer) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "type": + out.Type = string(in.String()) + case "cloud_provider": + out.CloudProvider = string(in.String()) + case "url": + out.URL = string(in.String()) + case "host": + out.Host = string(in.String()) + case "user_agent": + out.UserAgent = string(in.String()) + case "server": + out.Server = string(in.String()) + case "aws": + if in.IsNull() { + in.Skip() + out.AWS = nil + } else { + if out.AWS == nil { + out.AWS = new(AWSIMDSEventSerializer) + } + (*out.AWS).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(out *jwriter.Writer, in IMDSEventSerializer) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"type\":" + out.RawString(prefix[1:]) + out.String(string(in.Type)) + } + { + const prefix string = ",\"cloud_provider\":" + out.RawString(prefix) + out.String(string(in.CloudProvider)) + } + if in.URL != "" { + const prefix string = ",\"url\":" + out.RawString(prefix) + out.String(string(in.URL)) + } + if in.Host != "" { + const prefix string = ",\"host\":" + out.RawString(prefix) + out.String(string(in.Host)) + } + if in.UserAgent != "" { + const prefix string = ",\"user_agent\":" + out.RawString(prefix) + out.String(string(in.UserAgent)) + } + if in.Server != "" { + const prefix string = ",\"server\":" + out.RawString(prefix) + out.String(string(in.Server)) + } + if in.AWS != nil { + const prefix string = ",\"aws\":" + out.RawString(prefix) + (*in.AWS).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v IMDSEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(w, v) +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *IMDSEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(l, v) +} +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(in *jlexer.Lexer, out *ExitEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1044,7 +1195,7 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(i in.Consumed() } } -func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(out *jwriter.Writer, in ExitEventSerializer) { +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(out *jwriter.Writer, in ExitEventSerializer) { out.RawByte('{') first := true _ = first @@ -1063,14 +1214,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v ExitEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(w, v) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ExitEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers7(l, v) + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(l, v) } -func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(in *jlexer.Lexer, out *EventContextSerializer) { +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(in *jlexer.Lexer, out *EventContextSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1113,9 +1264,9 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(i out.MatchedRules = (out.MatchedRules)[:0] } for !in.IsDelim(']') { - var v18 MatchedRuleSerializer - (v18).UnmarshalEasyJSON(in) - out.MatchedRules = append(out.MatchedRules, v18) + var v21 MatchedRuleSerializer + (v21).UnmarshalEasyJSON(in) + out.MatchedRules = append(out.MatchedRules, v21) in.WantComma() } in.Delim(']') @@ -1132,7 +1283,7 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(i in.Consumed() } } -func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(out *jwriter.Writer, in EventContextSerializer) { +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(out *jwriter.Writer, in EventContextSerializer) { out.RawByte('{') first := true _ = first @@ -1182,11 +1333,11 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(o } { out.RawByte('[') - for v19, v20 := range in.MatchedRules { - if v19 > 0 { + for v22, v23 := range in.MatchedRules { + if v22 > 0 { out.RawByte(',') } - (v20).MarshalEasyJSON(out) + (v23).MarshalEasyJSON(out) } out.RawByte(']') } @@ -1206,14 +1357,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v EventContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(w, v) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *EventContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers8(l, v) + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(l, v) } -func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(in *jlexer.Lexer, out *DNSQuestionSerializer) { +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(in *jlexer.Lexer, out *DNSQuestionSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1252,7 +1403,7 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(i in.Consumed() } } -func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(out *jwriter.Writer, in DNSQuestionSerializer) { +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(out *jwriter.Writer, in DNSQuestionSerializer) { out.RawByte('{') first := true _ = first @@ -1286,14 +1437,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(o // MarshalEasyJSON supports easyjson.Marshaler interface func (v DNSQuestionSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(w, v) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *DNSQuestionSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers9(l, v) + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(l, v) } -func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(in *jlexer.Lexer, out *DNSEventSerializer) { +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(in *jlexer.Lexer, out *DNSEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1326,7 +1477,7 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10( in.Consumed() } } -func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(out *jwriter.Writer, in DNSEventSerializer) { +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(out *jwriter.Writer, in DNSEventSerializer) { out.RawByte('{') first := true _ = first @@ -1345,14 +1496,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10( // MarshalEasyJSON supports easyjson.Marshaler interface func (v DNSEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(w, v) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *DNSEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10(l, v) + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(l, v) } -func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(in *jlexer.Lexer, out *DDContextSerializer) { +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(in *jlexer.Lexer, out *DDContextSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1385,7 +1536,7 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11( in.Consumed() } } -func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(out *jwriter.Writer, in DDContextSerializer) { +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(out *jwriter.Writer, in DDContextSerializer) { out.RawByte('{') first := true _ = first @@ -1410,14 +1561,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11( // MarshalEasyJSON supports easyjson.Marshaler interface func (v DDContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(w, v) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *DDContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(l, v) + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(l, v) } -func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(in *jlexer.Lexer, out *ContainerContextSerializer) { +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(in *jlexer.Lexer, out *ContainerContextSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1462,7 +1613,7 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( in.Consumed() } } -func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(out *jwriter.Writer, in ContainerContextSerializer) { +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(out *jwriter.Writer, in ContainerContextSerializer) { out.RawByte('{') first := true _ = first @@ -1497,14 +1648,14 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( // MarshalEasyJSON supports easyjson.Marshaler interface func (v ContainerContextSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(w, v) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *ContainerContextSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12(l, v) + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(l, v) } -func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(in *jlexer.Lexer, out *BaseEventSerializer) { +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(in *jlexer.Lexer, out *BaseEventSerializer) { isTopLevel := in.IsStart() if in.IsNull() { if isTopLevel { @@ -1583,7 +1734,7 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13( in.Consumed() } } -func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(out *jwriter.Writer, in BaseEventSerializer) { +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(out *jwriter.Writer, in BaseEventSerializer) { out.RawByte('{') first := true _ = first @@ -1648,10 +1799,157 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13( // MarshalEasyJSON supports easyjson.Marshaler interface func (v BaseEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { - easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(w, v) + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(w, v) } // UnmarshalEasyJSON supports easyjson.Unmarshaler interface func (v *BaseEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { - easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers13(l, v) + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers14(l, v) +} +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(in *jlexer.Lexer, out *AWSSecurityCredentialsSerializer) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "code": + out.Code = string(in.String()) + case "type": + out.Type = string(in.String()) + case "access_key_id": + out.AccessKeyID = string(in.String()) + case "last_updated": + out.LastUpdated = string(in.String()) + case "expiration": + out.Expiration = string(in.String()) + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(out *jwriter.Writer, in AWSSecurityCredentialsSerializer) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"code\":" + out.RawString(prefix[1:]) + out.String(string(in.Code)) + } + { + const prefix string = ",\"type\":" + out.RawString(prefix) + out.String(string(in.Type)) + } + { + const prefix string = ",\"access_key_id\":" + out.RawString(prefix) + out.String(string(in.AccessKeyID)) + } + { + const prefix string = ",\"last_updated\":" + out.RawString(prefix) + out.String(string(in.LastUpdated)) + } + { + const prefix string = ",\"expiration\":" + out.RawString(prefix) + out.String(string(in.Expiration)) + } + out.RawByte('}') +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v AWSSecurityCredentialsSerializer) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(w, v) +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *AWSSecurityCredentialsSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers15(l, v) +} +func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(in *jlexer.Lexer, out *AWSIMDSEventSerializer) { + isTopLevel := in.IsStart() + if in.IsNull() { + if isTopLevel { + in.Consumed() + } + in.Skip() + return + } + in.Delim('{') + for !in.IsDelim('}') { + key := in.UnsafeFieldName(false) + in.WantColon() + if in.IsNull() { + in.Skip() + in.WantComma() + continue + } + switch key { + case "is_imds_v2": + out.IsIMDSv2 = bool(in.Bool()) + case "security_credentials": + if in.IsNull() { + in.Skip() + out.SecurityCredentials = nil + } else { + if out.SecurityCredentials == nil { + out.SecurityCredentials = new(AWSSecurityCredentialsSerializer) + } + (*out.SecurityCredentials).UnmarshalEasyJSON(in) + } + default: + in.SkipRecursive() + } + in.WantComma() + } + in.Delim('}') + if isTopLevel { + in.Consumed() + } +} +func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(out *jwriter.Writer, in AWSIMDSEventSerializer) { + out.RawByte('{') + first := true + _ = first + { + const prefix string = ",\"is_imds_v2\":" + out.RawString(prefix[1:]) + out.Bool(bool(in.IsIMDSv2)) + } + if in.SecurityCredentials != nil { + const prefix string = ",\"security_credentials\":" + out.RawString(prefix) + (*in.SecurityCredentials).MarshalEasyJSON(out) + } + out.RawByte('}') +} + +// MarshalEasyJSON supports easyjson.Marshaler interface +func (v AWSIMDSEventSerializer) MarshalEasyJSON(w *jwriter.Writer) { + easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(w, v) +} + +// UnmarshalEasyJSON supports easyjson.Unmarshaler interface +func (v *AWSIMDSEventSerializer) UnmarshalEasyJSON(l *jlexer.Lexer) { + easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers16(l, v) } diff --git a/pkg/security/serializers/serializers_linux.go b/pkg/security/serializers/serializers_linux.go index c1074c1547e8d0..287df2aeae699d 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -248,6 +248,8 @@ type ProcessSerializer struct { Source string `json:"source,omitempty"` // List of syscalls captured to generate the event Syscalls *SyscallsEventSerializer `json:"syscalls,omitempty"` + // List of AWS Security Credentials that the process had access to + AWSSecurityCredentials []*AWSSecurityCredentialsSerializer `json:"aws_security_credentials,omitempty"` } // FileEventSerializer serializes a file event to JSON @@ -492,6 +494,7 @@ type EventSerializer struct { *SignalEventSerializer `json:"signal,omitempty"` *SpliceEventSerializer `json:"splice,omitempty"` *DNSEventSerializer `json:"dns,omitempty"` + *IMDSEventSerializer `json:"imds,omitempty"` *BindEventSerializer `json:"bind,omitempty"` *MountEventSerializer `json:"mount,omitempty"` *SyscallsEventSerializer `json:"syscalls,omitempty"` @@ -625,6 +628,13 @@ func newProcessSerializer(ps *model.Process, e *model.Event) *ProcessSerializer psSerializer.UserSession = newUserSessionContextSerializer(&ps.UserSession, e) } + awsSecurityCredentials := e.FieldHandlers.ResolveAWSSecurityCredentials(e) + if len(awsSecurityCredentials) > 0 { + for _, creds := range awsSecurityCredentials { + psSerializer.AWSSecurityCredentials = append(psSerializer.AWSSecurityCredentials, newAWSSecurityCredentialsSerializer(&creds)) + } + } + if len(ps.ContainerID) != 0 { psSerializer.Container = &ContainerContextSerializer{ ID: ps.ContainerID, @@ -1187,6 +1197,9 @@ func NewEventSerializer(event *model.Event, opts *eval.Opts) *EventSerializer { case model.DNSEventType: s.EventContextSerializer.Outcome = serializeOutcome(0) s.DNSEventSerializer = newDNSEventSerializer(&event.DNS) + case model.IMDSEventType: + s.EventContextSerializer.Outcome = serializeOutcome(0) + s.IMDSEventSerializer = newIMDSEventSerializer(&event.IMDS) } return s diff --git a/pkg/security/serializers/serializers_linux_easyjson.go b/pkg/security/serializers/serializers_linux_easyjson.go index e4857136bcddc4..b9312defc37604 100644 --- a/pkg/security/serializers/serializers_linux_easyjson.go +++ b/pkg/security/serializers/serializers_linux_easyjson.go @@ -1096,6 +1096,37 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers10( in.Delim(']') } } + case "aws_security_credentials": + if in.IsNull() { + in.Skip() + out.AWSSecurityCredentials = nil + } else { + in.Delim('[') + if out.AWSSecurityCredentials == nil { + if !in.IsDelim(']') { + out.AWSSecurityCredentials = make([]*AWSSecurityCredentialsSerializer, 0, 8) + } else { + out.AWSSecurityCredentials = []*AWSSecurityCredentialsSerializer{} + } + } else { + out.AWSSecurityCredentials = (out.AWSSecurityCredentials)[:0] + } + for !in.IsDelim(']') { + var v12 *AWSSecurityCredentialsSerializer + if in.IsNull() { + in.Skip() + v12 = nil + } else { + if v12 == nil { + v12 = new(AWSSecurityCredentialsSerializer) + } + (*v12).UnmarshalEasyJSON(in) + } + out.AWSSecurityCredentials = append(out.AWSSecurityCredentials, v12) + in.WantComma() + } + in.Delim(']') + } default: in.SkipRecursive() } @@ -1226,11 +1257,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10( out.RawString(prefix) { out.RawByte('[') - for v12, v13 := range in.Args { - if v12 > 0 { + for v13, v14 := range in.Args { + if v13 > 0 { out.RawByte(',') } - out.String(string(v13)) + out.String(string(v14)) } out.RawByte(']') } @@ -1245,11 +1276,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10( out.RawString(prefix) { out.RawByte('[') - for v14, v15 := range in.Envs { - if v14 > 0 { + for v15, v16 := range in.Envs { + if v15 > 0 { out.RawByte(',') } - out.String(string(v15)) + out.String(string(v16)) } out.RawByte(']') } @@ -1286,11 +1317,29 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers10( out.RawString("null") } else { out.RawByte('[') - for v16, v17 := range *in.Syscalls { - if v16 > 0 { + for v17, v18 := range *in.Syscalls { + if v17 > 0 { + out.RawByte(',') + } + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(out, v18) + } + out.RawByte(']') + } + } + if len(in.AWSSecurityCredentials) != 0 { + const prefix string = ",\"aws_security_credentials\":" + out.RawString(prefix) + { + out.RawByte('[') + for v19, v20 := range in.AWSSecurityCredentials { + if v19 > 0 { out.RawByte(',') } - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(out, v17) + if v20 == nil { + out.RawString("null") + } else { + (*v20).MarshalEasyJSON(out) + } } out.RawByte(']') } @@ -1424,9 +1473,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( out.CapEffective = (out.CapEffective)[:0] } for !in.IsDelim(']') { - var v18 string - v18 = string(in.String()) - out.CapEffective = append(out.CapEffective, v18) + var v21 string + v21 = string(in.String()) + out.CapEffective = append(out.CapEffective, v21) in.WantComma() } in.Delim(']') @@ -1447,9 +1496,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( out.CapPermitted = (out.CapPermitted)[:0] } for !in.IsDelim(']') { - var v19 string - v19 = string(in.String()) - out.CapPermitted = append(out.CapPermitted, v19) + var v22 string + v22 = string(in.String()) + out.CapPermitted = append(out.CapPermitted, v22) in.WantComma() } in.Delim(']') @@ -1552,11 +1601,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( out.RawString("null") } else { out.RawByte('[') - for v20, v21 := range in.CapEffective { - if v20 > 0 { + for v23, v24 := range in.CapEffective { + if v23 > 0 { out.RawByte(',') } - out.String(string(v21)) + out.String(string(v24)) } out.RawByte(']') } @@ -1568,11 +1617,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers12( out.RawString("null") } else { out.RawByte('[') - for v22, v23 := range in.CapPermitted { - if v22 > 0 { + for v25, v26 := range in.CapPermitted { + if v25 > 0 { out.RawByte(',') } - out.String(string(v23)) + out.String(string(v26)) } out.RawByte(']') } @@ -1925,9 +1974,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers16( out.Argv = (out.Argv)[:0] } for !in.IsDelim(']') { - var v24 string - v24 = string(in.String()) - out.Argv = append(out.Argv, v24) + var v27 string + v27 = string(in.String()) + out.Argv = append(out.Argv, v27) in.WantComma() } in.Delim(']') @@ -1971,11 +2020,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers16( out.RawString(prefix) { out.RawByte('[') - for v25, v26 := range in.Argv { - if v25 > 0 { + for v28, v29 := range in.Argv { + if v28 > 0 { out.RawByte(',') } - out.String(string(v26)) + out.String(string(v29)) } out.RawByte(']') } @@ -2245,9 +2294,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( out.Flags = (out.Flags)[:0] } for !in.IsDelim(']') { - var v27 string - v27 = string(in.String()) - out.Flags = append(out.Flags, v27) + var v30 string + v30 = string(in.String()) + out.Flags = append(out.Flags, v30) in.WantComma() } in.Delim(']') @@ -2308,9 +2357,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( out.Hashes = (out.Hashes)[:0] } for !in.IsDelim(']') { - var v28 string - v28 = string(in.String()) - out.Hashes = append(out.Hashes, v28) + var v31 string + v31 = string(in.String()) + out.Hashes = append(out.Hashes, v31) in.WantComma() } in.Delim(']') @@ -2453,11 +2502,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( out.RawString(prefix) { out.RawByte('[') - for v29, v30 := range in.Flags { - if v29 > 0 { + for v32, v33 := range in.Flags { + if v32 > 0 { out.RawByte(',') } - out.String(string(v30)) + out.String(string(v33)) } out.RawByte(']') } @@ -2492,11 +2541,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers19( out.RawString(prefix) { out.RawByte('[') - for v31, v32 := range in.Hashes { - if v31 > 0 { + for v34, v35 := range in.Hashes { + if v34 > 0 { out.RawByte(',') } - out.String(string(v32)) + out.String(string(v35)) } out.RawByte(']') } @@ -2644,9 +2693,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers20( out.Flags = (out.Flags)[:0] } for !in.IsDelim(']') { - var v33 string - v33 = string(in.String()) - out.Flags = append(out.Flags, v33) + var v36 string + v36 = string(in.String()) + out.Flags = append(out.Flags, v36) in.WantComma() } in.Delim(']') @@ -2707,9 +2756,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers20( out.Hashes = (out.Hashes)[:0] } for !in.IsDelim(']') { - var v34 string - v34 = string(in.String()) - out.Hashes = append(out.Hashes, v34) + var v37 string + v37 = string(in.String()) + out.Hashes = append(out.Hashes, v37) in.WantComma() } in.Delim(']') @@ -2892,11 +2941,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers20( out.RawString(prefix) { out.RawByte('[') - for v35, v36 := range in.Flags { - if v35 > 0 { + for v38, v39 := range in.Flags { + if v38 > 0 { out.RawByte(',') } - out.String(string(v36)) + out.String(string(v39)) } out.RawByte(']') } @@ -2931,11 +2980,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers20( out.RawString(prefix) { out.RawByte('[') - for v37, v38 := range in.Hashes { - if v37 > 0 { + for v40, v41 := range in.Hashes { + if v40 > 0 { out.RawByte(',') } - out.String(string(v38)) + out.String(string(v41)) } out.RawByte(']') } @@ -2994,6 +3043,7 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers21( out.SignalEventSerializer = new(SignalEventSerializer) out.SpliceEventSerializer = new(SpliceEventSerializer) out.DNSEventSerializer = new(DNSEventSerializer) + out.IMDSEventSerializer = new(IMDSEventSerializer) out.BindEventSerializer = new(BindEventSerializer) out.MountEventSerializer = new(MountEventSerializer) out.SyscallsEventSerializer = new(SyscallsEventSerializer) @@ -3128,6 +3178,16 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers21( } (*out.DNSEventSerializer).UnmarshalEasyJSON(in) } + case "imds": + if in.IsNull() { + in.Skip() + out.IMDSEventSerializer = nil + } else { + if out.IMDSEventSerializer == nil { + out.IMDSEventSerializer = new(IMDSEventSerializer) + } + (*out.IMDSEventSerializer).UnmarshalEasyJSON(in) + } case "bind": if in.IsNull() { in.Skip() @@ -3171,9 +3231,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers21( *out.SyscallsEventSerializer = (*out.SyscallsEventSerializer)[:0] } for !in.IsDelim(']') { - var v39 SyscallSerializer - easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(in, &v39) - *out.SyscallsEventSerializer = append(*out.SyscallsEventSerializer, v39) + var v42 SyscallSerializer + easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(in, &v42) + *out.SyscallsEventSerializer = append(*out.SyscallsEventSerializer, v42) in.WantComma() } in.Delim(']') @@ -3365,6 +3425,16 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers21( } (*in.DNSEventSerializer).MarshalEasyJSON(out) } + if in.IMDSEventSerializer != nil { + const prefix string = ",\"imds\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + (*in.IMDSEventSerializer).MarshalEasyJSON(out) + } if in.BindEventSerializer != nil { const prefix string = ",\"bind\":" if first { @@ -3397,11 +3467,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers21( out.RawString("null") } else { out.RawByte('[') - for v40, v41 := range *in.SyscallsEventSerializer { - if v40 > 0 { + for v43, v44 := range *in.SyscallsEventSerializer { + if v43 > 0 { out.RawByte(',') } - easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(out, v41) + easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers11(out, v44) } out.RawByte(']') } @@ -3527,9 +3597,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers22( out.Tags = (out.Tags)[:0] } for !in.IsDelim(']') { - var v42 string - v42 = string(in.String()) - out.Tags = append(out.Tags, v42) + var v45 string + v45 = string(in.String()) + out.Tags = append(out.Tags, v45) in.WantComma() } in.Delim(']') @@ -3567,11 +3637,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers22( out.RawString("null") } else { out.RawByte('[') - for v43, v44 := range in.Tags { - if v43 > 0 { + for v46, v47 := range in.Tags { + if v46 > 0 { out.RawByte(',') } - out.String(string(v44)) + out.String(string(v47)) } out.RawByte(']') } @@ -3642,9 +3712,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers23( out.CapEffective = (out.CapEffective)[:0] } for !in.IsDelim(']') { - var v45 string - v45 = string(in.String()) - out.CapEffective = append(out.CapEffective, v45) + var v48 string + v48 = string(in.String()) + out.CapEffective = append(out.CapEffective, v48) in.WantComma() } in.Delim(']') @@ -3665,9 +3735,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers23( out.CapPermitted = (out.CapPermitted)[:0] } for !in.IsDelim(']') { - var v46 string - v46 = string(in.String()) - out.CapPermitted = append(out.CapPermitted, v46) + var v49 string + v49 = string(in.String()) + out.CapPermitted = append(out.CapPermitted, v49) in.WantComma() } in.Delim(']') @@ -3753,11 +3823,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers23( out.RawString("null") } else { out.RawByte('[') - for v47, v48 := range in.CapEffective { - if v47 > 0 { + for v50, v51 := range in.CapEffective { + if v50 > 0 { out.RawByte(',') } - out.String(string(v48)) + out.String(string(v51)) } out.RawByte(']') } @@ -3769,11 +3839,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers23( out.RawString("null") } else { out.RawByte('[') - for v49, v50 := range in.CapPermitted { - if v49 > 0 { + for v52, v53 := range in.CapPermitted { + if v52 > 0 { out.RawByte(',') } - out.String(string(v50)) + out.String(string(v53)) } out.RawByte(']') } @@ -3825,9 +3895,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( out.CapEffective = (out.CapEffective)[:0] } for !in.IsDelim(']') { - var v51 string - v51 = string(in.String()) - out.CapEffective = append(out.CapEffective, v51) + var v54 string + v54 = string(in.String()) + out.CapEffective = append(out.CapEffective, v54) in.WantComma() } in.Delim(']') @@ -3848,9 +3918,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( out.CapPermitted = (out.CapPermitted)[:0] } for !in.IsDelim(']') { - var v52 string - v52 = string(in.String()) - out.CapPermitted = append(out.CapPermitted, v52) + var v55 string + v55 = string(in.String()) + out.CapPermitted = append(out.CapPermitted, v55) in.WantComma() } in.Delim(']') @@ -3876,11 +3946,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( out.RawString("null") } else { out.RawByte('[') - for v53, v54 := range in.CapEffective { - if v53 > 0 { + for v56, v57 := range in.CapEffective { + if v56 > 0 { out.RawByte(',') } - out.String(string(v54)) + out.String(string(v57)) } out.RawByte(']') } @@ -3892,11 +3962,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers24( out.RawString("null") } else { out.RawByte('[') - for v55, v56 := range in.CapPermitted { - if v55 > 0 { + for v58, v59 := range in.CapPermitted { + if v58 > 0 { out.RawByte(',') } - out.String(string(v56)) + out.String(string(v59)) } out.RawByte(']') } @@ -4008,9 +4078,9 @@ func easyjsonDdc0fdbeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers26( out.Helpers = (out.Helpers)[:0] } for !in.IsDelim(']') { - var v57 string - v57 = string(in.String()) - out.Helpers = append(out.Helpers, v57) + var v60 string + v60 = string(in.String()) + out.Helpers = append(out.Helpers, v60) in.WantComma() } in.Delim(']') @@ -4075,11 +4145,11 @@ func easyjsonDdc0fdbeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers26( } { out.RawByte('[') - for v58, v59 := range in.Helpers { - if v58 > 0 { + for v61, v62 := range in.Helpers { + if v61 > 0 { out.RawByte(',') } - out.String(string(v59)) + out.String(string(v62)) } out.RawByte(']') } diff --git a/pkg/security/tests/activity_dumps_common.go b/pkg/security/tests/activity_dumps_common.go index c6eede24864f6c..0de24f7165c107 100644 --- a/pkg/security/tests/activity_dumps_common.go +++ b/pkg/security/tests/activity_dumps_common.go @@ -27,7 +27,7 @@ const ( ) var ( - testActivityDumpDuration = time.Second * 30 + testActivityDumpDuration = time.Minute * 10 testActivityDumpLoadControllerPeriod = time.Second * 10 ) diff --git a/pkg/security/tests/activity_dumps_loadcontroller_test.go b/pkg/security/tests/activity_dumps_loadcontroller_test.go index f307c513aa148c..af6d7c106d8ca1 100644 --- a/pkg/security/tests/activity_dumps_loadcontroller_test.go +++ b/pkg/security/tests/activity_dumps_loadcontroller_test.go @@ -9,6 +9,7 @@ package tests import ( + "fmt" "os" "path/filepath" "slices" @@ -39,7 +40,7 @@ func TestActivityDumpsLoadControllerTimeout(t *testing.T) { outputDir := t.TempDir() expectedFormats := []string{"json", "protobuf"} - testActivityDumpTracedEventTypes := []string{"exec", "open", "syscalls", "dns", "bind"} + testActivityDumpTracedEventTypes := []string{"exec", "open", "syscalls", "dns", "bind", "imds"} opts := testOpts{ enableActivityDump: true, activityDumpRateLimiter: testActivityDumpRateLimiter, @@ -51,6 +52,7 @@ func TestActivityDumpsLoadControllerTimeout(t *testing.T) { activityDumpTracedEventTypes: testActivityDumpTracedEventTypes, activityDumpLoadControllerPeriod: testActivityDumpLoadControllerPeriod, activityDumpLoadControllerTimeout: time.Minute, + networkIngressEnabled: true, } test, err := newTestModule(t, nil, []*rules.RuleDefinition{}, withStaticOpts(opts)) if err != nil { @@ -104,7 +106,7 @@ func TestActivityDumpsLoadControllerEventTypes(t *testing.T) { outputDir := t.TempDir() expectedFormats := []string{"json", "protobuf"} - testActivityDumpTracedEventTypes := []string{"exec", "open", "syscalls", "dns", "bind"} + testActivityDumpTracedEventTypes := []string{"exec", "open", "syscalls", "dns", "bind", "imds"} test, err := newTestModule(t, nil, []*rules.RuleDefinition{}, withStaticOpts(testOpts{ enableActivityDump: true, activityDumpRateLimiter: testActivityDumpRateLimiter, @@ -115,6 +117,7 @@ func TestActivityDumpsLoadControllerEventTypes(t *testing.T) { activityDumpLocalStorageFormats: expectedFormats, activityDumpTracedEventTypes: testActivityDumpTracedEventTypes, activityDumpLoadControllerPeriod: testActivityDumpLoadControllerPeriod, + networkIngressEnabled: true, })) if err != nil { t.Fatal(err) @@ -125,6 +128,11 @@ func TestActivityDumpsLoadControllerEventTypes(t *testing.T) { t.Fatal(err) } + goSyscallTester, err := loadSyscallTester(t, test, "syscall_go_tester") + if err != nil { + t.Fatal(err) + } + // first, stop all running activity dumps err = test.StopAllActivityDumps() if err != nil { @@ -137,6 +145,20 @@ func TestActivityDumpsLoadControllerEventTypes(t *testing.T) { } defer dockerInstance.stop() + // setup IMDS interface + cmd := dockerInstance.Command(goSyscallTester, []string{"-setup-imds-test"}, []string{}) + _, err = cmd.CombinedOutput() + if err != nil { + t.Fatalf("ERROR: %v", err) + } + defer func() { + cleanup := dockerInstance.Command(goSyscallTester, []string{"-cleanup-imds-test"}, []string{}) + _, err := cleanup.CombinedOutput() + if err != nil { + fmt.Printf("failed to cleanup IMDS test: %v", err) + } + }() + for activeEventTypes := activitydump.TracedEventTypesReductionOrder; ; activeEventTypes = activeEventTypes[1:] { testName := "" for i, activeEventType := range activeEventTypes { @@ -150,7 +172,7 @@ func TestActivityDumpsLoadControllerEventTypes(t *testing.T) { } t.Run(testName, func(t *testing.T) { // add all event types to the dump - test.addAllEventTypesOnDump(dockerInstance, syscallTester) + test.addAllEventTypesOnDump(dockerInstance, syscallTester, goSyscallTester) time.Sleep(time.Second * 3) // trigger reducer test.triggerLoadControllerReducer(dockerInstance, dump) @@ -174,7 +196,7 @@ func TestActivityDumpsLoadControllerEventTypes(t *testing.T) { activeTypes = append(activeTypes, model.FileOpenEventType) } if !isEventTypesStringSlicesEqual(activeTypes, presentEventTypes) { - t.Fatalf("Dump's event types are different as expected (%v) vs (%v)", activeEventTypes, presentEventTypes) + t.Fatalf("Dump's event types don't match: expected[%v] vs observed[%v]", activeEventTypes, presentEventTypes) } dump = nextDump }) @@ -202,7 +224,7 @@ func TestActivityDumpsLoadControllerRateLimiter(t *testing.T) { outputDir := t.TempDir() expectedFormats := []string{"json", "protobuf"} - testActivityDumpTracedEventTypes := []string{"exec", "open", "syscalls", "dns", "bind"} + testActivityDumpTracedEventTypes := []string{"exec", "open", "syscalls", "dns", "bind", "imds"} test, err := newTestModule(t, nil, []*rules.RuleDefinition{}, withStaticOpts(testOpts{ enableActivityDump: true, activityDumpRateLimiter: testActivityDumpRateLimiter, @@ -213,6 +235,7 @@ func TestActivityDumpsLoadControllerRateLimiter(t *testing.T) { activityDumpLocalStorageFormats: expectedFormats, activityDumpTracedEventTypes: testActivityDumpTracedEventTypes, activityDumpLoadControllerPeriod: testActivityDumpLoadControllerPeriod, + networkIngressEnabled: true, })) if err != nil { t.Fatal(err) diff --git a/pkg/security/tests/activity_dumps_test.go b/pkg/security/tests/activity_dumps_test.go index 3f983763a835a6..eff9be42b6d681 100644 --- a/pkg/security/tests/activity_dumps_test.go +++ b/pkg/security/tests/activity_dumps_test.go @@ -10,6 +10,7 @@ package tests import ( "fmt" + imdsutils "github.com/DataDog/datadog-agent/pkg/security/tests/imds_utils" "os" "path/filepath" "strings" @@ -44,7 +45,7 @@ func TestActivityDumps(t *testing.T) { outputDir := t.TempDir() expectedFormats := []string{"json", "protobuf"} - testActivityDumpTracedEventTypes := []string{"exec", "open", "syscalls", "dns", "bind"} + testActivityDumpTracedEventTypes := []string{"exec", "open", "syscalls", "dns", "bind", "imds"} test, err := newTestModule(t, nil, []*rules.RuleDefinition{}, withStaticOpts(testOpts{ enableActivityDump: true, activityDumpRateLimiter: testActivityDumpRateLimiter, @@ -55,6 +56,7 @@ func TestActivityDumps(t *testing.T) { activityDumpLocalStorageFormats: expectedFormats, activityDumpTracedEventTypes: testActivityDumpTracedEventTypes, activityDumpCleanupPeriod: testActivityDumpCleanupPeriod, + networkIngressEnabled: true, })) if err != nil { t.Fatal(err) @@ -65,6 +67,57 @@ func TestActivityDumps(t *testing.T) { t.Fatal(err) } + goSyscallTester, err := loadSyscallTester(t, test, "syscall_go_tester") + if err != nil { + t.Fatal(err) + } + + t.Run("activity-dump-cgroup-imds", func(t *testing.T) { + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets. See dns_test.go + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + dockerInstance, dump, err := test.StartADockerGetDump() + if err != nil { + t.Fatal(err) + } + defer dockerInstance.stop() + + time.Sleep(time.Second * 1) // to ensure we did not get ratelimited + cmd := dockerInstance.Command(goSyscallTester, []string{"-setup-and-run-imds-test"}, []string{}) + _, err = cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + time.Sleep(1 * time.Second) // a quick sleep to let events to be added to the dump + + err = test.StopActivityDump(dump.Name, "", "") + if err != nil { + t.Fatal(err) + } + + validateActivityDumpOutputs(t, test, expectedFormats, dump.OutputFiles, func(ad *activitydump.ActivityDump) bool { + nodes := ad.FindMatchingRootNodes(goSyscallTester) + if nodes == nil { + t.Fatal("Node not found in activity dump") + } + + var requestFound, responseFound bool + for _, node := range nodes { + for evt := range node.IMDSEvents { + if evt.Type == "request" && evt.URL == imdsutils.IMDSSecurityCredentialsURL { + requestFound = true + } + if evt.Type == "response" && evt.AWS.SecurityCredentials.AccessKeyID == imdsutils.AWSSecurityCredentialsAccessKeyIDTestValue { + responseFound = true + } + } + } + return requestFound && responseFound + }, nil) + }) + t.Run("activity-dump-cgroup-process", func(t *testing.T) { dockerInstance, dump, err := test.StartADockerGetDump() if err != nil { diff --git a/pkg/security/tests/cmdwrapper.go b/pkg/security/tests/cmdwrapper.go index deba4fbad1cfa5..ce572e64653116 100644 --- a/pkg/security/tests/cmdwrapper.go +++ b/pkg/security/tests/cmdwrapper.go @@ -98,7 +98,7 @@ func (d *dockerCmdWrapper) Command(bin string, args []string, envs []string) *ex func (d *dockerCmdWrapper) start() ([]byte, error) { d.containerName = fmt.Sprintf("docker-wrapper-%s", utils.RandString(6)) - cmd := exec.Command(d.executable, "run", "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined", "--rm", "-d", "--name", d.containerName, "-v", d.mountSrc+":"+d.mountDest, d.image, "sleep", "1200") + cmd := exec.Command(d.executable, "run", "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined", "--rm", "--cap-add", "NET_ADMIN", "-d", "--name", d.containerName, "-v", d.mountSrc+":"+d.mountDest, d.image, "sleep", "1200") out, err := cmd.CombinedOutput() if err == nil { d.containerID = strings.TrimSpace(string(out)) diff --git a/pkg/security/tests/filters_test.go b/pkg/security/tests/filters_test.go index 7c52f6392a54b4..a2b398848e7126 100644 --- a/pkg/security/tests/filters_test.go +++ b/pkg/security/tests/filters_test.go @@ -367,7 +367,10 @@ func TestFilterDiscarderMask(t *testing.T) { if _, _, errno := syscall.Syscall(syscallNB, uintptr(testFilePtr), uintptr(unsafe.Pointer(utimbuf)), 0); errno != 0 { t.Fatal(error(errno)) } - if err := waitForProbeEvent(test, nil, "utimes.file.path", testFile, model.FileUtimesEventType); err != nil { + if err := waitForProbeEvent(test, nil, model.FileUtimesEventType, eventKeyValueFilter{ + key: "utimes.file.path", + value: testFile, + }); err != nil { t.Fatal("should get a utimes event") } @@ -377,7 +380,10 @@ func TestFilterDiscarderMask(t *testing.T) { if _, _, errno := syscall.Syscall(syscallNB, uintptr(testFilePtr), uintptr(unsafe.Pointer(utimbuf)), 0); errno != 0 { t.Fatal(error(errno)) } - if err := waitForProbeEvent(test, nil, "utimes.file.path", testFile, model.FileUtimesEventType); err == nil { + if err := waitForProbeEvent(test, nil, model.FileUtimesEventType, eventKeyValueFilter{ + key: "utimes.file.path", + value: testFile, + }); err == nil { t.Fatal("shouldn't get a utimes event") } diff --git a/pkg/security/tests/imds_test.go b/pkg/security/tests/imds_test.go new file mode 100644 index 00000000000000..0d0b450c19b7f8 --- /dev/null +++ b/pkg/security/tests/imds_test.go @@ -0,0 +1,737 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build linux && functionaltests + +// Package tests holds tests related files +package tests + +import ( + "fmt" + "github.com/DataDog/datadog-agent/pkg/config" + "github.com/DataDog/datadog-agent/pkg/security/ebpf/kernel" + "github.com/DataDog/datadog-agent/pkg/security/secl/model" + "github.com/DataDog/datadog-agent/pkg/security/secl/rules" + "github.com/DataDog/datadog-agent/pkg/security/tests/imds_utils" + "github.com/stretchr/testify/assert" + "net/http" + "os" + "path" + "syscall" + "testing" +) + +func TestAWSIMDSv1Request(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_rule_aws_imds_v1_request", + Expression: fmt.Sprintf(`imds.cloud_provider == "aws" && imds.aws.is_imds_v2 == false && imds.type == "request" && process.file.name == "%s"`, path.Base(executable)), + }, + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: true})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + t.Run("aws_imds_v1_request", func(t *testing.T) { + test.WaitSignal(t, func() error { + response, err := http.Get(fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL)) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_rule_aws_imds_v1_request") + assert.Equal(t, "request", event.IMDS.Type, "wrong IMDS request type") + assert.Equal(t, imdsServerAddr, event.IMDS.Host, "wrong IMDS request Host") + assert.Equal(t, imdsutils.IMDSSecurityCredentialsURL, event.IMDS.URL, "wrong IMDS request URL") + assert.Equal(t, "Go-http-client/1.1", event.IMDS.UserAgent, "wrong IMDS request user agent") + + test.validateIMDSSchema(t, event) + }) + }) +} + +func TestAWSIMDSv1Response(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + // this rule is added first on purpose to double-check that an IMDSv2 event doesn't trigger and IMDSv1 rule + { + ID: "test_rule_aws_imds_v2_request", + Expression: fmt.Sprintf(`imds.cloud_provider == "aws" && imds.aws.is_imds_v2 == true && imds.type == "request" && process.file.name == "%s"`, path.Base(executable)), + }, + { + ID: "test_rule_aws_imds_v1_response", + Expression: fmt.Sprintf(`imds.cloud_provider == "aws" && imds.aws.is_imds_v2 == false && imds.type == "response" && process.file.name == "%s"`, path.Base(executable)), + }, + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: true})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + t.Run("aws_imds_v1_response", func(t *testing.T) { + test.WaitSignal(t, func() error { + response, err := http.Get(fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL)) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_rule_aws_imds_v1_response") + assert.Equal(t, "response", event.IMDS.Type, "wrong IMDS request Type") + assert.Equal(t, imdsutils.AWSIMDSServerTestValue, event.IMDS.Server, "wrong IMDS request Server") + assert.Equal(t, imdsutils.AWSSecurityCredentialsTypeTestValue, event.IMDS.AWS.SecurityCredentials.Type, "wrong IMDS request AWS Security Credentials Type") + assert.Equal(t, imdsutils.AWSSecurityCredentialsExpirationTestValue, event.IMDS.AWS.SecurityCredentials.ExpirationRaw, "wrong IMDS request AWS Security Credentials ExpirationRaw") + assert.Equal(t, imdsutils.AWSSecurityCredentialsAccessKeyIDTestValue, event.IMDS.AWS.SecurityCredentials.AccessKeyID, "wrong IMDS request AWS Security Credentials AccessKeyID") + assert.Equal(t, imdsutils.AWSSecurityCredentialsCodeTestValue, event.IMDS.AWS.SecurityCredentials.Code, "wrong IMDS request AWS Security Credentials Code") + assert.Equal(t, imdsutils.AWSSecurityCredentialsLastUpdatedTestValue, event.IMDS.AWS.SecurityCredentials.LastUpdated, "wrong IMDS request AWS Security Credentials LastUpdated") + + test.validateIMDSSchema(t, event) + }) + }) +} + +func TestNoAWSIMDSv1Response(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_rule_aws_imds_v1_response", + Expression: fmt.Sprintf(`imds.cloud_provider == "aws" && imds.aws.is_imds_v2 == false && imds.type == "response" && process.file.name == "%s"`, path.Base(executable)), + }, + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: false})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + t.Run("no_aws_imds_v1_response", func(t *testing.T) { + if err := waitForIMDSResponseProbeEvent(test, func() error { + response, err := http.Get(fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL)) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + return nil + }, path.Base(executable)); err == nil { + t.Fatal("shouldn't get an event") + } + }) +} + +func TestAWSIMDSv2Request(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + // this rule is added first on purpose to double-check that an IMDSv2 event doesn't trigger and IMDSv1 rule + { + ID: "test_rule_aws_imds_v1_response", + Expression: fmt.Sprintf(`imds.cloud_provider == "aws" && imds.aws.is_imds_v2 == false && imds.type == "response" && process.file.name == "%s"`, path.Base(executable)), + }, + { + ID: "test_rule_aws_imds_v2_request", + Expression: fmt.Sprintf(`imds.cloud_provider == "aws" && imds.aws.is_imds_v2 == true && imds.type == "request" && process.file.name == "%s"`, path.Base(executable)), + }, + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: true})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + t.Run("aws_imds_v2_request", func(t *testing.T) { + test.WaitSignal(t, func() error { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL), nil) + if err != nil { + return fmt.Errorf("failed to instantiate request: %v", err) + } + req.Header.Set("X-aws-ec2-metadata-token", "my_secret_token") + response, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_rule_aws_imds_v2_request") + assert.Equal(t, "request", event.IMDS.Type, "wrong IMDS request type") + assert.Equal(t, imdsServerAddr, event.IMDS.Host, "wrong IMDS request Host") + assert.Equal(t, imdsutils.IMDSSecurityCredentialsURL, event.IMDS.URL, "wrong IMDS request URL") + assert.Equal(t, "Go-http-client/1.1", event.IMDS.UserAgent, "wrong IMDS request user agent") + + test.validateIMDSSchema(t, event) + }) + }) +} + +func TestGCPIMDS(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_rule_gcp_imds_request", + Expression: fmt.Sprintf(`imds.cloud_provider == "gcp" && imds.type == "request" && process.file.name == "%s"`, path.Base(executable)), + }, + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: true})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + t.Run("gcp_imds_request", func(t *testing.T) { + test.WaitSignal(t, func() error { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL), nil) + if err != nil { + return fmt.Errorf("failed to instantiate request: %v", err) + } + req.Header.Set("Metadata-Flavor", "Google") + response, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_rule_gcp_imds_request") + assert.Equal(t, "request", event.IMDS.Type, "wrong IMDS request type") + assert.Equal(t, imdsServerAddr, event.IMDS.Host, "wrong IMDS request Host") + assert.Equal(t, imdsutils.IMDSSecurityCredentialsURL, event.IMDS.URL, "wrong IMDS request URL") + assert.Equal(t, "Go-http-client/1.1", event.IMDS.UserAgent, "wrong IMDS request user agent") + + test.validateIMDSSchema(t, event) + }) + }) +} + +func TestAzureIMDS(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_rule_azure_imds_request", + Expression: fmt.Sprintf(`imds.cloud_provider == "azure" && imds.type == "request" && process.file.name == "%s"`, path.Base(executable)), + }, + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: true})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + t.Run("azure_imds_request", func(t *testing.T) { + test.WaitSignal(t, func() error { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL), nil) + if err != nil { + return fmt.Errorf("failed to instantiate request: %v", err) + } + req.Header.Set("Metadata", "true") + response, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_rule_azure_imds_request") + assert.Equal(t, "request", event.IMDS.Type, "wrong IMDS request type") + assert.Equal(t, imdsServerAddr, event.IMDS.Host, "wrong IMDS request Host") + assert.Equal(t, imdsutils.IMDSSecurityCredentialsURL, event.IMDS.URL, "wrong IMDS request URL") + assert.Equal(t, "Go-http-client/1.1", event.IMDS.UserAgent, "wrong IMDS request user agent") + + test.validateIMDSSchema(t, event) + }) + }) +} + +func TestIBMIMDS(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_rule_idbm_imds_request", + Expression: fmt.Sprintf(`imds.cloud_provider == "ibm" && imds.type == "request" && process.file.name == "%s"`, path.Base(executable)), + }, + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: true})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + t.Run("ibm_imds_request", func(t *testing.T) { + test.WaitSignal(t, func() error { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL), nil) + if err != nil { + return fmt.Errorf("failed to instantiate request: %v", err) + } + req.Header.Set("Metadata-Flavor", "ibm") + response, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_rule_idbm_imds_request") + assert.Equal(t, "request", event.IMDS.Type, "wrong IMDS request type") + assert.Equal(t, imdsServerAddr, event.IMDS.Host, "wrong IMDS request Host") + assert.Equal(t, imdsutils.IMDSSecurityCredentialsURL, event.IMDS.URL, "wrong IMDS request URL") + assert.Equal(t, "Go-http-client/1.1", event.IMDS.UserAgent, "wrong IMDS request user agent") + + test.validateIMDSSchema(t, event) + }) + }) +} + +func TestOracleIMDS(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_rule_oracle_imds_request", + Expression: fmt.Sprintf(`imds.cloud_provider == "oracle" && imds.type == "request" && process.file.name == "%s"`, path.Base(executable)), + }, + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: true})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + t.Run("oracle_imds_request", func(t *testing.T) { + test.WaitSignal(t, func() error { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL), nil) + if err != nil { + return fmt.Errorf("failed to instantiate request: %v", err) + } + req.Header.Set("Authorization", "Bearer Oracle") + response, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + return nil + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_rule_oracle_imds_request") + assert.Equal(t, "request", event.IMDS.Type, "wrong IMDS request type") + assert.Equal(t, imdsServerAddr, event.IMDS.Host, "wrong IMDS request Host") + assert.Equal(t, imdsutils.IMDSSecurityCredentialsURL, event.IMDS.URL, "wrong IMDS request URL") + assert.Equal(t, "Go-http-client/1.1", event.IMDS.UserAgent, "wrong IMDS request user agent") + + test.validateIMDSSchema(t, event) + }) + }) +} + +func TestIMDSProcessContext(t *testing.T) { + SkipIfNotAvailable(t) + + checkKernelCompatibility(t, "RHEL, SLES and Oracle kernels", func(kv *kernel.Version) bool { + // TODO: Oracle because we are missing offsets + return kv.IsRH7Kernel() || kv.IsOracleUEKKernel() || kv.IsSLESKernel() + }) + + if testEnvironment != DockerEnvironment && !config.IsContainerized() { + if out, err := loadModule("veth"); err != nil { + t.Fatalf("couldn't load 'veth' module: %s,%v", string(out), err) + } + } + + executable, err := os.Executable() + if err != nil { + t.Fatal(err) + } + + ruleDefs := []*rules.RuleDefinition{ + { + ID: "test_rule_oracle_imds_request", + Expression: fmt.Sprintf(`imds.cloud_provider == "oracle" && imds.type == "request" && process.file.name == "%s"`, path.Base(executable)), + }, + { + ID: "test_imds_process_context", + Expression: `open.file.path == "{{.Root}}/test-open" && open.flags & O_CREAT != 0`, + }, + // check dumps + } + + // create dummy interface + dummy, err := imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) + if err != nil { + t.Fatal(err) + } + defer func() { + if err = imdsutils.RemoveDummyInterface(dummy); err != nil { + t.Fatal(err) + } + }() + + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err = imdsutils.StopIMDSserver(imdsServer); err != nil { + t.Fatal(err) + } + }() + + test, err := newTestModule(t, nil, ruleDefs, withStaticOpts(testOpts{networkIngressEnabled: true})) + if err != nil { + t.Fatal(err) + } + defer test.Close() + + testFile, testFilePtr, err := test.Path("test-open") + if err != nil { + t.Fatal(err) + } + + t.Run("imds_process_context", ifSyscallSupported("SYS_OPEN", func(t *testing.T, syscallNB uintptr) { + defer os.Remove(testFile) + + test.WaitSignal(t, func() error { + // make request first to populate process cache + response, err := http.Get(fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL)) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + defer response.Body.Close() + + fd, _, errno := syscall.Syscall(syscallNB, uintptr(testFilePtr), syscall.O_CREAT, 0755) + if errno != 0 { + return error(errno) + } + return syscall.Close(int(fd)) + }, func(event *model.Event, rule *rules.Rule) { + assertTriggeredRule(t, rule, "test_imds_process_context") + + // check if the process has the correct IMDS credentials context + assert.NotNil(t, event.ProcessCacheEntry.Process.AWSSecurityCredentials, "empty IMDS context") + if len(event.ProcessCacheEntry.Process.AWSSecurityCredentials) > 0 { + creds := event.ProcessCacheEntry.Process.AWSSecurityCredentials[0] + assert.Equal(t, imdsutils.AWSSecurityCredentialsTypeTestValue, creds.Type, "wrong IMDS context AWS Security Credentials Type") + assert.Equal(t, imdsutils.AWSSecurityCredentialsExpirationTestValue, creds.ExpirationRaw, "wrong IMDS context AWS Security Credentials ExpirationRaw") + assert.Equal(t, imdsutils.AWSSecurityCredentialsAccessKeyIDTestValue, creds.AccessKeyID, "wrong IMDS context AWS Security Credentials AccessKeyID") + assert.Equal(t, imdsutils.AWSSecurityCredentialsCodeTestValue, creds.Code, "wrong IMDS context AWS Security Credentials Code") + assert.Equal(t, imdsutils.AWSSecurityCredentialsLastUpdatedTestValue, creds.LastUpdated, "wrong IMDS context AWS Security Credentials LastUpdated") + } + + test.validateOpenSchema(t, event) + }) + })) +} diff --git a/pkg/security/tests/imds_utils/imds_utils.go b/pkg/security/tests/imds_utils/imds_utils.go new file mode 100644 index 00000000000000..c4e2ee5ee3b253 --- /dev/null +++ b/pkg/security/tests/imds_utils/imds_utils.go @@ -0,0 +1,138 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +// Package imdsutils holds utils related to the IMDS tests +package imdsutils + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "net/http" + "time" + + "github.com/vishvananda/netlink" +) + +const ( + // IMDSSecurityCredentialsURL is the URL used by the IMDS tests + IMDSSecurityCredentialsURL = "/latest/meta-data/iam/security-credentials/test" + // AWSSecurityCredentialsAccessKeyIDTestValue is the AccessKeyID used by the IMDS tests + AWSSecurityCredentialsAccessKeyIDTestValue = "ASIAIOSFODNN7EXAMPLE" + // AWSSecurityCredentialsTypeTestValue is the AWS Credentials Type used by the IMDS tests + AWSSecurityCredentialsTypeTestValue = "AWS-HMAC" + // AWSSecurityCredentialsCodeTestValue is the AWS Credentials Code used by the IMDS tests + AWSSecurityCredentialsCodeTestValue = "Success" + // AWSSecurityCredentialsLastUpdatedTestValue is the AWS Credentials LastUpdated value used by the IMDS tests + AWSSecurityCredentialsLastUpdatedTestValue = "2012-04-26T16:39:16Z" + // AWSSecurityCredentialsExpirationTestValue is the AWS Credentials Expiration value used by the IMDS tests + AWSSecurityCredentialsExpirationTestValue = "2324-05-01T12:00:00Z" + // AWSIMDSServerTestValue is the IMDS Server used by the IMDS tests + AWSIMDSServerTestValue = "EC2ws" + // CSMDummyInterface is the Dummy interface name used by the IMDS tests + CSMDummyInterface = "dummy_csm" + // IMDSTestServerIP is the IMDS server IP used by the IMDS tests + IMDSTestServerIP = "169.254.169.254" + // IMDSTestServerPort is the IMDS server port used by the IMDS tests + IMDSTestServerPort = 8080 +) + +// CreateDummyInterface creates a dummy interface and attaches it to the provided IP +func CreateDummyInterface(ip string, name string) (*netlink.Dummy, error) { + dummy := &netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: name, + }, + } + + // delete existing dummy interface + _ = netlink.LinkDel(dummy) + + // Add the dummy interface + if err := netlink.LinkAdd(dummy); err != nil { + return nil, fmt.Errorf("failed to create dummy interface %s: %v", name, err) + } + + // attach the IMDS IP to the dummy interface + addr := &netlink.Addr{IPNet: &net.IPNet{ + IP: net.ParseIP(ip), + Mask: net.CIDRMask(24, 32), + }} + if err := netlink.AddrAdd(dummy, addr); err != nil { + return nil, fmt.Errorf("failed to attach IMDS IP to %s: %v", name, err) + } + + // set dummy interface up + if err := netlink.LinkSetUp(dummy); err != nil { + return nil, fmt.Errorf("failed to set %s up: %v", name, err) + } + + return dummy, nil +} + +// RemoveDummyInterface removes the provided dummy interface +func RemoveDummyInterface(link *netlink.Dummy) error { + if err := netlink.LinkDel(link); err != nil { + return fmt.Errorf("failed to delete %s: %v", link.Name, err) + } + return nil +} + +// CreateIMDSServer creates a fake IMDS server +func CreateIMDSServer(addr string) *http.Server { + mux := http.NewServeMux() + mux.HandleFunc(IMDSSecurityCredentialsURL, func(w http.ResponseWriter, r *http.Request) { + // Define your custom JSON data + data := map[string]interface{}{ + "AccessKeyId": AWSSecurityCredentialsAccessKeyIDTestValue, + "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "Token": "FQoDYXdzEL3//////////wEaDNzv2bUBTBHZWpL6iWLZAyaMGJnKlXNoDMHEFvgF7OeM8Cxz69tJNYk8GvIYVpOInuLeMfkplcQ2NeO6xVBa0gB0T6f/5AWhdV5SdpDoyCtYIvMDIG2a7DJVpuZ7d7vylWfFzohpV5Y7C7gWQuIdH/qc3kkWz3hkhCc+5iKmB+QxG30BPoCGOYYzN+QkGiPjZzXfTFdAfX/+/VY6DiVnl8MGB2TFdSRpF7GbcuhKhrkAnJ7UlNnnYVVtFfO9TlBMSbJH55CFv0FDACG0nHsIExSkD1Vau/nHeFLv6xMT+WAtI05/RtZZC8JfKJi4ST+TqB5ftc2qVLMy9AlWzrr2uN6R1fSeOESO7rf2Koq3m31CR8KKjYMXdo/38dNwxawf+3z8U8HhBc5sYXfcWHH7m0Q7DqQ3pPzMKFL/QPxTssP9lwJr2L7vqJxqN4Tcjq9+8pg=", + "Expiration": AWSSecurityCredentialsExpirationTestValue, + "Code": AWSSecurityCredentialsCodeTestValue, + "LastUpdated": AWSSecurityCredentialsLastUpdatedTestValue, + "Type": AWSSecurityCredentialsTypeTestValue, + } + + // Convert data to JSON + response, err := json.Marshal(data) + if err != nil { + http.Error(w, "couldn't marshal data", http.StatusInternalServerError) + return + } + + // Set Content-Type header to application/json + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Server", AWSIMDSServerTestValue) + + // Write JSON response + w.Write(response) + }) + + server := &http.Server{ + Addr: addr, + Handler: mux, + } + + go func() { + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Printf("HTTP server error: %v", err) + } + }() + + return server +} + +// StopIMDSserver stops the provided server gracefully +func StopIMDSserver(server *http.Server) error { + shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second) + defer shutdownRelease() + + if err := server.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("failed to shutdown server: %v", err) + } + return nil +} diff --git a/pkg/security/tests/module_tester.go b/pkg/security/tests/module_tester.go index b284a165e07c35..ba444f150fe875 100644 --- a/pkg/security/tests/module_tester.go +++ b/pkg/security/tests/module_tester.go @@ -765,6 +765,7 @@ func genTestConfigs(cfgDir string, opts testOpts) (*emconfig.Config, *secconfig. "SBOMEnabled": opts.enableSBOM, "EBPFLessEnabled": ebpfLessEnabled, "FIMEnabled": opts.enableFIM, // should only be enabled/disabled on windows + "NetworkIngressEnabled": opts.networkIngressEnabled, }); err != nil { return nil, nil, err } diff --git a/pkg/security/tests/module_tester_linux.go b/pkg/security/tests/module_tester_linux.go index 108b74f33a5fb8..2c9fbc017ce227 100644 --- a/pkg/security/tests/module_tester_linux.go +++ b/pkg/security/tests/module_tester_linux.go @@ -79,6 +79,8 @@ event_monitoring_config: - "*custom*" network: enabled: true + ingress: + enabled: {{ .NetworkIngressEnabled }} flush_discarder_window: 0 {{if .DisableFilters}} enable_kernel_filters: false @@ -1046,23 +1048,48 @@ func ifSyscallSupported(syscall string, test func(t *testing.T, syscallNB uintpt } } +// eventKeyValueFilter is used to filter events in `waitForProbeEvent` +type eventKeyValueFilter struct { + key string + value interface{} +} + // waitForProbeEvent returns the first open event with the provided filename. // WARNING: this function may yield a "fatal error: concurrent map writes" error if the ruleset of testModule does not // contain a rule on "open.file.path" // //nolint:deadcode,unused -func waitForProbeEvent(test *testModule, action func() error, key string, value interface{}, eventType model.EventType) error { +func waitForProbeEvent(test *testModule, action func() error, eventType model.EventType, filters ...eventKeyValueFilter) error { return test.GetProbeEvent(action, func(event *model.Event) bool { - if v, _ := event.GetFieldValue(key); v == value { - return true + for _, filter := range filters { + if v, _ := event.GetFieldValue(filter.key); v != filter.value { + return false + } } - return false + return true }, getEventTimeout, eventType) } //nolint:deadcode,unused func waitForOpenProbeEvent(test *testModule, action func() error, filename string) error { - return waitForProbeEvent(test, action, "open.file.path", filename, model.FileOpenEventType) + return waitForProbeEvent(test, action, model.FileOpenEventType, eventKeyValueFilter{ + key: "open.file.path", + value: filename, + }) +} + +//nolint:deadcode,unused +func waitForIMDSResponseProbeEvent(test *testModule, action func() error, processFileName string) error { + return waitForProbeEvent(test, action, model.IMDSEventType, []eventKeyValueFilter{ + { + key: "process.file.name", + value: processFileName, + }, + { + key: "imds.type", + value: "response", + }, + }...) } //nolint:deadcode,unused @@ -1315,7 +1342,7 @@ func (tm *testModule) findCgroupDump(id *activityDumpIdentifier) *activityDumpId } //nolint:deadcode,unused -func (tm *testModule) addAllEventTypesOnDump(dockerInstance *dockerCmdWrapper, syscallTester string) { +func (tm *testModule) addAllEventTypesOnDump(dockerInstance *dockerCmdWrapper, syscallTester string, goSyscallTester string) { // open cmd := dockerInstance.Command("touch", []string{filepath.Join(tm.Root(), "open")}, []string{}) _, _ = cmd.CombinedOutput() @@ -1329,6 +1356,10 @@ func (tm *testModule) addAllEventTypesOnDump(dockerInstance *dockerCmdWrapper, s _, _ = cmd.CombinedOutput() // syscalls should be added with previous events + + // imds + cmd = dockerInstance.Command(goSyscallTester, []string{"-run-imds-test"}, []string{}) + _, _ = cmd.CombinedOutput() } //nolint:deadcode,unused @@ -1407,6 +1438,16 @@ func searchForDNS(ad *dump.ActivityDump) bool { return false } +//nolint:deadcode,unused +func searchForIMDS(ad *dump.ActivityDump) bool { + for _, node := range ad.ActivityTree.ProcessNodes { + if len(node.IMDSEvents) > 0 { + return true + } + } + return false +} + //nolint:deadcode,unused func searchForBind(ad *dump.ActivityDump) bool { for _, node := range ad.ActivityTree.ProcessNodes { @@ -1500,6 +1541,9 @@ func (tm *testModule) extractAllDumpEventTypes(id *activityDumpIdentifier) ([]st if searchForOpen(ad) { res = append(res, "open") } + if searchForIMDS(ad) { + res = append(res, "imds") + } return res, nil } diff --git a/pkg/security/tests/network_device_test.go b/pkg/security/tests/network_device_test.go index 8cebc4ca12e1e5..e264d7005d7d40 100644 --- a/pkg/security/tests/network_device_test.go +++ b/pkg/security/tests/network_device_test.go @@ -49,7 +49,7 @@ func TestNetDevice(t *testing.T) { Expression: `dns.question.type == A && dns.question.name == "google.com" && process.file.name == "testsuite"`, } - test, err := newTestModule(t, nil, []*rules.RuleDefinition{rule}) + test, err := newTestModule(t, nil, []*rules.RuleDefinition{rule}, withStaticOpts(testOpts{networkIngressEnabled: true})) if err != nil { t.Fatal(err) } @@ -160,7 +160,7 @@ func TestTCFilters(t *testing.T) { Expression: `dns.question.type == A`, } - test, err := newTestModule(t, nil, []*rules.RuleDefinition{rule}) + test, err := newTestModule(t, nil, []*rules.RuleDefinition{rule}, withStaticOpts(testOpts{networkIngressEnabled: true})) if err != nil { t.Fatal(err) } diff --git a/pkg/security/tests/schemas.go b/pkg/security/tests/schemas.go index 4adc47d492fbf2..9954c40b0b941f 100644 --- a/pkg/security/tests/schemas.go +++ b/pkg/security/tests/schemas.go @@ -286,6 +286,12 @@ func (tm *testModule) validateDNSSchema(t *testing.T, event *model.Event) bool { return tm.validateEventSchema(t, event, "file:///schemas/dns.schema.json") } +//nolint:deadcode,unused +func (tm *testModule) validateIMDSSchema(t *testing.T, event *model.Event) bool { + t.Helper() + return tm.validateEventSchema(t, event, "file:///schemas/imds.schema.json") +} + //nolint:deadcode,unused func (tm *testModule) validateBindSchema(t *testing.T, event *model.Event) bool { t.Helper() diff --git a/pkg/security/tests/schemas/imds.schema.json b/pkg/security/tests/schemas/imds.schema.json new file mode 100644 index 00000000000000..2ac17ef236ad73 --- /dev/null +++ b/pkg/security/tests/schemas/imds.schema.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "imds.json", + "type": "object", + "allOf": [ + { + "$ref": "/schemas/event.json" + }, + { + "$ref": "/schemas/usr.json" + }, + { + "$ref": "/schemas/process_context.json" + }, + { + "$ref": "/schemas/network.json" + }, + { + "date": { + "$ref": "/schemas/datetime.json" + } + }, + { + "properties": { + "imds": { + "type": "object", + "required": [ + "cloud_provider", + "type" + ], + "properties": { + "type": { + "type": "string" + }, + "cloud_provider": { + "type": "string" + }, + "host": { + "type": "string" + }, + "url": { + "type": "string" + }, + "user_agent": { + "type": "string" + }, + "server": { + "type": "string" + }, + "aws": { + "type": "object", + "required": [ + "is_imds_v2" + ], + "properties": { + "is_imds_v2": { + "type": "boolean" + }, + "security_credentials": { + "type": "object", + "required": [ + "code", + "type", + "access_key_id", + "last_updated", + "expiration" + ], + "properties": { + "code": { + "type": "string" + }, + "type": { + "type": "string" + }, + "access_key_id": { + "type": "string" + }, + "last_updated": { + "type": "string" + }, + "expiration": { + "type": "string" + } + } + } + } + } + } + } + } + } + ] +} diff --git a/pkg/security/tests/syscall_tester/go/syscall_go_tester.go b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go index 90dd1eb9e01f61..7b89560c9d72ae 100644 --- a/pkg/security/tests/syscall_tester/go/syscall_go_tester.go +++ b/pkg/security/tests/syscall_tester/go/syscall_go_tester.go @@ -13,14 +13,17 @@ import ( _ "embed" "flag" "fmt" + "net/http" + "time" manager "github.com/DataDog/ebpf-manager" "github.com/syndtr/gocapability/capability" + "github.com/vishvananda/netlink" authenticationv1 "k8s.io/api/authentication/v1" "github.com/DataDog/datadog-agent/cmd/cws-instrumentation/subcommands/injectcmd" - "github.com/DataDog/datadog-agent/pkg/security/resolvers/usersessions" + imdsutils "github.com/DataDog/datadog-agent/pkg/security/tests/imds_utils" ) var ( @@ -28,6 +31,10 @@ var ( bpfClone bool capsetProcessCreds bool k8sUserSession bool + setupAndRunIMDSTest bool + setupIMDSTest bool + cleanupIMDSTest bool + runIMDSTest bool userSessionExecutable string userSessionOpenPath string ) @@ -134,6 +141,48 @@ func K8SUserSessionTest(executable string, openPath string) error { return nil } +func SetupAndRunIMDSTest() error { + // create dummy interface + dummy, err := SetupIMDSTest() + defer func() { + if err = CleanupIMDSTest(dummy); err != nil { + panic(err) + } + }() + + return RunIMDSTest() +} + +func RunIMDSTest() error { + // create fake IMDS server + imdsServerAddr := fmt.Sprintf("%s:%v", imdsutils.IMDSTestServerIP, imdsutils.IMDSTestServerPort) + imdsServer := imdsutils.CreateIMDSServer(imdsServerAddr) + defer func() { + if err := imdsutils.StopIMDSserver(imdsServer); err != nil { + panic(err) + } + }() + + // give some time for the server to start + time.Sleep(5 * time.Second) + + // make IMDS request + response, err := http.Get(fmt.Sprintf("http://%s%s", imdsServerAddr, imdsutils.IMDSSecurityCredentialsURL)) + if err != nil { + return fmt.Errorf("failed to query IMDS server: %v", err) + } + return response.Body.Close() +} + +func SetupIMDSTest() (*netlink.Dummy, error) { + // create dummy interface + return imdsutils.CreateDummyInterface(imdsutils.IMDSTestServerIP, imdsutils.CSMDummyInterface) +} + +func CleanupIMDSTest(dummy *netlink.Dummy) error { + return imdsutils.RemoveDummyInterface(dummy) +} + func main() { flag.BoolVar(&bpfLoad, "load-bpf", false, "load the eBPF progams") flag.BoolVar(&bpfClone, "clone-bpf", false, "clone maps") @@ -141,6 +190,10 @@ func main() { flag.BoolVar(&k8sUserSession, "k8s-user-session", false, "user session test") flag.StringVar(&userSessionExecutable, "user-session-executable", "", "executable used for the user session test") flag.StringVar(&userSessionOpenPath, "user-session-open-path", "", "file used for the user session test") + flag.BoolVar(&setupAndRunIMDSTest, "setup-and-run-imds-test", false, "when set, runs the IMDS test by creating a dummy interface, binding a fake IMDS server to it and sending an IMDS request") + flag.BoolVar(&setupIMDSTest, "setup-imds-test", false, "when set, creates a dummy interface and attach the IMDS IP to it") + flag.BoolVar(&cleanupIMDSTest, "cleanup-imds-test", false, "when set, removes the dummy interface of the IMDS test") + flag.BoolVar(&runIMDSTest, "run-imds-test", false, "when set, binds an IMDS server locally and sends a query to it") flag.Parse() @@ -161,4 +214,32 @@ func main() { panic(err) } } + + if setupAndRunIMDSTest { + if err := SetupAndRunIMDSTest(); err != nil { + panic(err) + } + } + + if setupIMDSTest { + if _, err := SetupIMDSTest(); err != nil { + panic(err) + } + } + + if cleanupIMDSTest { + if err := CleanupIMDSTest(&netlink.Dummy{ + LinkAttrs: netlink.LinkAttrs{ + Name: imdsutils.CSMDummyInterface, + }, + }); err != nil { + panic(err) + } + } + + if runIMDSTest { + if err := RunIMDSTest(); err != nil { + panic(err) + } + } } diff --git a/pkg/security/tests/testopts.go b/pkg/security/tests/testopts.go index ddc0a6d3b1a041..6d731b2f2c418d 100644 --- a/pkg/security/tests/testopts.go +++ b/pkg/security/tests/testopts.go @@ -57,6 +57,7 @@ type testOpts struct { tagsResolver tags.Resolver snapshotRuleMatchHandler func(*testModule, *model.Event, *rules.Rule) enableFIM bool // only valid on windows + networkIngressEnabled bool } type dynamicTestOpts struct { @@ -125,5 +126,6 @@ func (to testOpts) Equal(opts testOpts) bool { to.disableRuntimeSecurity == opts.disableRuntimeSecurity && to.enableSBOM == opts.enableSBOM && to.snapshotRuleMatchHandler == nil && opts.snapshotRuleMatchHandler == nil && - to.preStartCallback == nil && opts.preStartCallback == nil + to.preStartCallback == nil && opts.preStartCallback == nil && + to.networkIngressEnabled == opts.networkIngressEnabled } diff --git a/releasenotes/notes/runtime-security-imds-event-4369a6ca5a4a97a6.yaml b/releasenotes/notes/runtime-security-imds-event-4369a6ca5a4a97a6.yaml new file mode 100644 index 00000000000000..68bb1f6e956c12 --- /dev/null +++ b/releasenotes/notes/runtime-security-imds-event-4369a6ca5a4a97a6.yaml @@ -0,0 +1,11 @@ +# Each section from every release note are combined when the +# CHANGELOG.rst is rendered. So the text needs to be worded so that +# it does not depend on any information only available in another +# section. This may mean repeating some details, but each section +# must be readable independently of the other. +# +# Each section note must be formatted as reStructuredText. +--- +features: + - | + CSM captures and generates events based on IMDS traffic From cbffc41be534959b84002f3a692ed02c76667851 Mon Sep 17 00:00:00 2001 From: Yoann Ghigoff Date: Fri, 24 May 2024 11:43:31 +0200 Subject: [PATCH 10/32] [CWS] limit process tree depth when serializing events (#25822) --- docs/cloud-workload-security/backend.md | 9 +++++++++ docs/cloud-workload-security/backend.schema.json | 4 ++++ pkg/security/serializers/serializers_base.go | 2 ++ .../serializers/serializers_base_linux_easyjson.go | 12 ++++++++++++ pkg/security/serializers/serializers_linux.go | 12 ++++++++++++ 5 files changed, 39 insertions(+) diff --git a/docs/cloud-workload-security/backend.md b/docs/cloud-workload-security/backend.md index 734ab8652a5e72..72d861274dda3b 100644 --- a/docs/cloud-workload-security/backend.md +++ b/docs/cloud-workload-security/backend.md @@ -1212,6 +1212,10 @@ CSM Threats logs have the following JSON schema: "variables": { "$ref": "#/$defs/Variables", "description": "Variables values" + }, + "truncated_ancestors": { + "type": "boolean", + "description": "True if the ancestors list was truncated because it was too big" } }, "additionalProperties": false, @@ -3346,6 +3350,10 @@ CSM Threats logs have the following JSON schema: "variables": { "$ref": "#/$defs/Variables", "description": "Variables values" + }, + "truncated_ancestors": { + "type": "boolean", + "description": "True if the ancestors list was truncated because it was too big" } }, "additionalProperties": false, @@ -3393,6 +3401,7 @@ CSM Threats logs have the following JSON schema: | `parent` | Parent process | | `ancestors` | Ancestor processes | | `variables` | Variables values | +| `truncated_ancestors` | True if the ancestors list was truncated because it was too big | | References | | ---------- | diff --git a/docs/cloud-workload-security/backend.schema.json b/docs/cloud-workload-security/backend.schema.json index 028408bfdc9c74..7876acf46c134e 100644 --- a/docs/cloud-workload-security/backend.schema.json +++ b/docs/cloud-workload-security/backend.schema.json @@ -1196,6 +1196,10 @@ "variables": { "$ref": "#/$defs/Variables", "description": "Variables values" + }, + "truncated_ancestors": { + "type": "boolean", + "description": "True if the ancestors list was truncated because it was too big" } }, "additionalProperties": false, diff --git a/pkg/security/serializers/serializers_base.go b/pkg/security/serializers/serializers_base.go index 6bafe842564d6a..c77943f740419e 100644 --- a/pkg/security/serializers/serializers_base.go +++ b/pkg/security/serializers/serializers_base.go @@ -73,6 +73,8 @@ type ProcessContextSerializer struct { Ancestors []*ProcessSerializer `json:"ancestors,omitempty"` // Variables values Variables Variables `json:"variables,omitempty"` + // True if the ancestors list was truncated because it was too big + TruncatedAncestors bool `json:"truncated_ancestors,omitempty"` } // IPPortSerializer is used to serialize an IP and Port context to JSON diff --git a/pkg/security/serializers/serializers_base_linux_easyjson.go b/pkg/security/serializers/serializers_base_linux_easyjson.go index 9612ce25cdcdaa..9d23a6305fefbf 100644 --- a/pkg/security/serializers/serializers_base_linux_easyjson.go +++ b/pkg/security/serializers/serializers_base_linux_easyjson.go @@ -146,6 +146,8 @@ func easyjsonA1e47abeDecodeGithubComDataDogDatadogAgentPkgSecuritySerializers1(i } case "variables": (out.Variables).UnmarshalEasyJSON(in) + case "truncated_ancestors": + out.TruncatedAncestors = bool(in.Bool()) case "pid": out.Pid = uint32(in.Uint32()) case "ppid": @@ -435,6 +437,16 @@ func easyjsonA1e47abeEncodeGithubComDataDogDatadogAgentPkgSecuritySerializers1(o } (in.Variables).MarshalEasyJSON(out) } + if in.TruncatedAncestors { + const prefix string = ",\"truncated_ancestors\":" + if first { + first = false + out.RawString(prefix[1:]) + } else { + out.RawString(prefix) + } + out.Bool(bool(in.TruncatedAncestors)) + } if in.Pid != 0 { const prefix string = ",\"pid\":" if first { diff --git a/pkg/security/serializers/serializers_linux.go b/pkg/security/serializers/serializers_linux.go index 287df2aeae699d..b7443e47949b29 100644 --- a/pkg/security/serializers/serializers_linux.go +++ b/pkg/security/serializers/serializers_linux.go @@ -24,6 +24,8 @@ import ( "github.com/DataDog/datadog-agent/pkg/security/utils" ) +const processTreeMaxDepth = 200 + // FileSerializer serializes a file to JSON // easyjson:json type FileSerializer struct { @@ -909,6 +911,16 @@ func newProcessContextSerializer(pc *model.ProcessContext, e *model.Event) *Proc ptr = it.Next() } + // shrink the middle of the ancestors list if it is too long + if len(ps.Ancestors) > processTreeMaxDepth { + subLen := processTreeMaxDepth / 2 + // we add one extra element to the leaf slice in case processTreeMaxDepth is an odd number + // this is to make sure the length of the resulting slice matches the value of processTreeMaxDepth + extraElem := processTreeMaxDepth % 2 + ps.Ancestors = append(ps.Ancestors[0:subLen+extraElem], ps.Ancestors[len(ps.Ancestors)-subLen:]...) + ps.TruncatedAncestors = true + } + return &ps } From 3cc76cdbbc05b7d001a485082e98a8f13d75eb2d Mon Sep 17 00:00:00 2001 From: Arthur Bellal Date: Fri, 24 May 2024 11:43:36 +0200 Subject: [PATCH 11/32] (fleet) add minimal e2e coverage for the installer daemon (#25873) * (fleet) add minimal e2e coverage for the installer daemon This PR: - fixes 2 remaining bugs preventing the installer daemon from starting without manual help - adds an e2e to ensure that behavior * remove no_agent * Update pkg/fleet/installer/service/datadog_installer.go Co-authored-by: Baptiste Foy --------- Co-authored-by: Baptiste Foy --- pkg/config/setup/config_nix.go | 5 ++++- pkg/fleet/installer/service/datadog_installer.go | 9 +++++++++ test/new-e2e/tests/installer/host/host.go | 11 ++++++++++- .../tests/installer/package_installer_test.go | 12 ++++++++++++ 4 files changed, 35 insertions(+), 2 deletions(-) diff --git a/pkg/config/setup/config_nix.go b/pkg/config/setup/config_nix.go index 7ef3114861bcd9..50a7788e487c53 100644 --- a/pkg/config/setup/config_nix.go +++ b/pkg/config/setup/config_nix.go @@ -14,10 +14,13 @@ var ( // InstallPath is the default install path for the agent // It might be overridden at build time InstallPath = "/opt/datadog-agent" + + // defaultRunPath is the default path for the agent's runtime files + // It might be overridden at build time + defaultRunPath = "/opt/datadog-agent/run" ) var ( - defaultRunPath = filepath.Join(InstallPath, "run") // defaultSystemProbeAddress is the default unix socket path to be used for connecting to the system probe defaultSystemProbeAddress = filepath.Join(InstallPath, "run/sysprobe.sock") // defaultEventMonitorAddress is the default unix socket path to be used for connecting to the event monitor diff --git a/pkg/fleet/installer/service/datadog_installer.go b/pkg/fleet/installer/service/datadog_installer.go index 22223f30aa847a..f3319a587cd28c 100644 --- a/pkg/fleet/installer/service/datadog_installer.go +++ b/pkg/fleet/installer/service/datadog_installer.go @@ -181,6 +181,15 @@ func getAgentIDs() (uid, gid int, err error) { // startInstallerStable starts the stable systemd units for the installer func startInstallerStable(ctx context.Context) (err error) { + _, err = os.Stat("/etc/datadog-agent/datadog.yaml") + if err != nil && !os.IsNotExist(err) { + return err + } + // this is expected during a fresh install with the install script / asible / chef / etc... + // the config is populated afterwards by the install method and the agent is restarted + if os.IsNotExist(err) { + return nil + } return startUnit(ctx, installerUnit) } diff --git a/test/new-e2e/tests/installer/host/host.go b/test/new-e2e/tests/installer/host/host.go index 78dbc96197cdbc..9a4eca0272026d 100644 --- a/test/new-e2e/tests/installer/host/host.go +++ b/test/new-e2e/tests/installer/host/host.go @@ -486,7 +486,7 @@ func (s *State) AssertUnitsEnabled(names ...string) { for _, name := range names { unit, ok := s.Units[name] assert.True(s.t, ok, "unit %v is not enabled", name) - assert.NotEqual(s.t, "unknown", unit.Enabled, "unit %v is not enabled", name) + assert.Equal(s.t, "enabled", unit.Enabled, "unit %v is not enabled", name) } } @@ -507,6 +507,15 @@ func (s *State) AssertUnitsNotLoaded(names ...string) { } } +// AssertUnitsNotEnabled asserts that a systemd unit is not enabled +func (s *State) AssertUnitsNotEnabled(names ...string) { + for _, name := range names { + unit, ok := s.Units[name] + assert.True(s.t, ok, "unit %v is enabled", name) + assert.Equal(s.t, "disabled", unit.Enabled, "unit %v is enabled", name) + } +} + // AssertUnitsDead asserts that a systemd unit is not running. func (s *State) AssertUnitsDead(names ...string) { for _, name := range names { diff --git a/test/new-e2e/tests/installer/package_installer_test.go b/test/new-e2e/tests/installer/package_installer_test.go index f4e486ed17b62c..396aea99c2dbd0 100644 --- a/test/new-e2e/tests/installer/package_installer_test.go +++ b/test/new-e2e/tests/installer/package_installer_test.go @@ -51,6 +51,18 @@ func (s *packageInstallerSuite) TestInstall() { state.AssertUnitsNotLoaded("datadog-installer.service", "datadog-installer-exp.service") } +func (s *packageInstallerSuite) TestInstallWithRemoteUpdates() { + s.RunInstallScript("DD_REMOTE_UPDATES=true") + defer s.Purge() + s.host.WaitForUnitActive("datadog-installer.service") + + state := s.host.State() + state.AssertUnitsLoaded("datadog-installer.service", "datadog-installer-exp.service") + state.AssertUnitsEnabled("datadog-installer.service") + state.AssertUnitsNotEnabled("datadog-installer-exp.service") + state.AssertUnitsRunning("datadog-installer.service") +} + func (s *packageInstallerSuite) TestUninstall() { s.RunInstallScript("DD_NO_AGENT_INSTALL=true") s.Purge() From 6afeaa75b40e89d00b294d5a39de536ad4d5d4f1 Mon Sep 17 00:00:00 2001 From: AliDatadog <125997632+AliDatadog@users.noreply.github.com> Date: Fri, 24 May 2024 11:46:50 +0200 Subject: [PATCH 12/32] [CONTINT-4112] Add PodWatcher in the autoscaling controller (#25321) Co-authored-by: vboulineau --- .../internal/kubeapiserver/kubeapiserver.go | 2 +- comp/core/workloadmeta/component.go | 2 +- .../autoscaling/workload/pod_watcher.go | 161 +++++++++++++++ .../autoscaling/workload/pod_watcher_test.go | 183 ++++++++++++++++++ 4 files changed, 346 insertions(+), 2 deletions(-) create mode 100644 pkg/clusteragent/autoscaling/workload/pod_watcher.go create mode 100644 pkg/clusteragent/autoscaling/workload/pod_watcher_test.go diff --git a/comp/core/workloadmeta/collectors/internal/kubeapiserver/kubeapiserver.go b/comp/core/workloadmeta/collectors/internal/kubeapiserver/kubeapiserver.go index 54da56e24684cd..c32c77a97f6859 100644 --- a/comp/core/workloadmeta/collectors/internal/kubeapiserver/kubeapiserver.go +++ b/comp/core/workloadmeta/collectors/internal/kubeapiserver/kubeapiserver.go @@ -35,7 +35,7 @@ type storeGenerator func(context.Context, workloadmeta.Component, kubernetes.Int func storeGenerators(cfg config.Reader) []storeGenerator { generators := []storeGenerator{newNodeStore} - if cfg.GetBool("cluster_agent.collect_kubernetes_tags") { + if cfg.GetBool("cluster_agent.collect_kubernetes_tags") || cfg.GetBool("autoscaling.workload.enabled") { generators = append(generators, newPodStore) } diff --git a/comp/core/workloadmeta/component.go b/comp/core/workloadmeta/component.go index 342e5d4cd6f679..652dc9b5748d60 100644 --- a/comp/core/workloadmeta/component.go +++ b/comp/core/workloadmeta/component.go @@ -32,7 +32,7 @@ type Component interface { // evolves or as information about the entity is reported from multiple // sources (such as a container runtime and an orchestrator). // - // See the documentation for EventBundle regarding appropropriate handling + // See the documentation for EventBundle regarding appropriate handling // for messages on this channel. Subscribe(name string, priority SubscriberPriority, filter *Filter) chan EventBundle diff --git a/pkg/clusteragent/autoscaling/workload/pod_watcher.go b/pkg/clusteragent/autoscaling/workload/pod_watcher.go new file mode 100644 index 00000000000000..e4d2e2c9d9a821 --- /dev/null +++ b/pkg/clusteragent/autoscaling/workload/pod_watcher.go @@ -0,0 +1,161 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build kubeapiserver + +package workload + +import ( + "context" + "sync" + + "github.com/DataDog/datadog-agent/comp/core/workloadmeta" + "github.com/DataDog/datadog-agent/pkg/util/kubernetes" + "github.com/DataDog/datadog-agent/pkg/util/log" +) + +// NamespacedPodOwner represents a pod owner in a namespace +type NamespacedPodOwner struct { + // Namespace is the namespace of the pod owner + Namespace string + // Kind is the kind of the pod owner (e.g. Deployment, StatefulSet etc.) + // ReplicaSet is replaced by Deployment + Kind string + // Name is the name of the pod owner + Name string +} + +type podWatcher struct { + mutex sync.RWMutex + wlm workloadmeta.Component + podsPerPodOwner map[NamespacedPodOwner]map[string]*workloadmeta.KubernetesPod +} + +// PodWatcher indexes pods by their owner +type PodWatcher interface { + // Start starts the PodWatcher. + Start(ctx context.Context) + // GetPodsForOwner returns the pods for the given owner. + GetPodsForOwner(NamespacedPodOwner) []*workloadmeta.KubernetesPod +} + +// newPodWatcher creates a new PodWatcher +func newPodWatcher(wlm workloadmeta.Component) PodWatcher { + return &podWatcher{ + wlm: wlm, + podsPerPodOwner: make(map[NamespacedPodOwner]map[string]*workloadmeta.KubernetesPod), + } +} + +// GetPodsForOwner returns the pods for the given owner. +func (pw *podWatcher) GetPodsForOwner(owner NamespacedPodOwner) []*workloadmeta.KubernetesPod { + pw.mutex.RLock() + defer pw.mutex.RUnlock() + pods, ok := pw.podsPerPodOwner[owner] + if !ok { + return nil + } + res := make([]*workloadmeta.KubernetesPod, 0, len(pods)) + for _, pod := range pods { + res = append(res, pod) + } + return res +} + +// Start subscribes to workloadmeta events and indexes pods by their owner. +func (pw *podWatcher) Start(ctx context.Context) { + log.Debug("Starting PodWatcher") + filterParams := workloadmeta.FilterParams{ + Kinds: []workloadmeta.Kind{workloadmeta.KindKubernetesPod}, + Source: workloadmeta.SourceAll, + EventType: workloadmeta.EventTypeAll, + } + ch := pw.wlm.Subscribe( + "app-autoscaler-pod-watcher", + workloadmeta.NormalPriority, + workloadmeta.NewFilter(&filterParams), + ) + defer pw.wlm.Unsubscribe(ch) + + for { + select { + case <-ctx.Done(): + log.Debugf("Stopping PodWatcher") + return + case eventBundle, more := <-ch: + eventBundle.Acknowledge() + if !more { + log.Debugf("Stopping PodWatcher") + return + } + for _, event := range eventBundle.Events { + pw.handleEvent(event) + } + } + } +} + +func (pw *podWatcher) handleEvent(event workloadmeta.Event) { + pw.mutex.Lock() + defer pw.mutex.Unlock() + pod, ok := event.Entity.(*workloadmeta.KubernetesPod) + if !ok { + log.Debugf("Ignoring event with entity type %T", event.Entity) + return + } + if len(pod.Owners) == 0 { + log.Debugf("Ignoring pod %s without owner", pod.Name) + return + } + switch event.Type { + case workloadmeta.EventTypeSet: + pw.handleSetEvent(pod) + case workloadmeta.EventTypeUnset: + pw.handleUnsetEvent(pod) + default: + log.Errorf("Ignoring event type %d", event.Type) + } +} + +func (pw *podWatcher) handleSetEvent(pod *workloadmeta.KubernetesPod) { + podOwner := getNamespacedPodOwner(pod.Namespace, &pod.Owners[0]) + log.Debugf("Adding pod %s to owner %s", pod.ID, podOwner) + if _, ok := pw.podsPerPodOwner[podOwner]; !ok { + pw.podsPerPodOwner[podOwner] = make(map[string]*workloadmeta.KubernetesPod) + } + pw.podsPerPodOwner[podOwner][pod.ID] = pod +} + +func (pw *podWatcher) handleUnsetEvent(pod *workloadmeta.KubernetesPod) { + podOwner := getNamespacedPodOwner(pod.Namespace, &pod.Owners[0]) + if podOwner.Name == "" { + log.Debugf("Ignoring pod %s without owner name", pod.Name) + return + } + log.Debugf("Removing pod %s from owner %s", pod.ID, podOwner) + if _, ok := pw.podsPerPodOwner[podOwner]; !ok { + return + } + delete(pw.podsPerPodOwner[podOwner], pod.ID) + if len(pw.podsPerPodOwner[podOwner]) == 0 { + delete(pw.podsPerPodOwner, podOwner) + } +} + +func getNamespacedPodOwner(ns string, owner *workloadmeta.KubernetesPodOwner) NamespacedPodOwner { + res := NamespacedPodOwner{ + Name: owner.Name, + Kind: owner.Kind, + Namespace: ns, + } + if res.Kind == kubernetes.ReplicaSetKind { + deploymentName := kubernetes.ParseDeploymentForReplicaSet(res.Name) + if deploymentName != "" { + res.Kind = kubernetes.DeploymentKind + res.Name = deploymentName + } + } + return res +} diff --git a/pkg/clusteragent/autoscaling/workload/pod_watcher_test.go b/pkg/clusteragent/autoscaling/workload/pod_watcher_test.go new file mode 100644 index 00000000000000..f833bb21269714 --- /dev/null +++ b/pkg/clusteragent/autoscaling/workload/pod_watcher_test.go @@ -0,0 +1,183 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build kubeapiserver && test + +package workload + +import ( + "context" + "testing" + "time" + + "github.com/DataDog/datadog-agent/comp/core/config" + "github.com/DataDog/datadog-agent/comp/core/log/logimpl" + "github.com/DataDog/datadog-agent/comp/core/workloadmeta" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" + "github.com/DataDog/datadog-agent/pkg/util/kubernetes" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/fx" +) + +func TestHandleSetEvent(t *testing.T) { + pw := newPodWatcher(nil).(*podWatcher) + pod := &workloadmeta.KubernetesPod{ + EntityID: workloadmeta.EntityID{ + Kind: workloadmeta.KindKubernetesPod, + ID: "p1", + }, + EntityMeta: workloadmeta.EntityMeta{ + Name: "pod1", + Namespace: "default", + }, + Owners: []workloadmeta.KubernetesPodOwner{{Kind: kubernetes.ReplicaSetKind, Name: "deploymentName-766dbb7846"}}, + } + event := workloadmeta.Event{ + Type: workloadmeta.EventTypeSet, + Entity: pod, + } + + pw.handleEvent(event) + + expectedOwner := NamespacedPodOwner{ + Namespace: "default", + Kind: kubernetes.DeploymentKind, + Name: "deploymentName", + } + pods := pw.GetPodsForOwner(expectedOwner) + require.Len(t, pods, 1) + assert.Equal(t, pod, pods[0]) +} + +func TestHandleUnsetEvent(t *testing.T) { + pw := newPodWatcher(nil).(*podWatcher) + pod := &workloadmeta.KubernetesPod{ + EntityID: workloadmeta.EntityID{ + Kind: workloadmeta.KindKubernetesPod, + ID: "p1", + }, + EntityMeta: workloadmeta.EntityMeta{ + Name: "pod1", + Namespace: "default", + }, + Owners: []workloadmeta.KubernetesPodOwner{{Kind: kubernetes.ReplicaSetKind, Name: "deploymentName-766dbb7846"}}, + } + setEvent := workloadmeta.Event{ + Type: workloadmeta.EventTypeSet, + Entity: pod, + } + unsetEvent := workloadmeta.Event{ + Type: workloadmeta.EventTypeUnset, + Entity: pod, + } + + pw.handleEvent(setEvent) + pw.handleEvent(unsetEvent) + + pods := pw.GetPodsForOwner(NamespacedPodOwner{ + Namespace: "default", + Kind: kubernetes.DeploymentKind, + Name: "deploymentName", + }) + assert.Nil(t, pods) + assert.NotNil(t, pw.podsPerPodOwner) +} + +func TestPodWatcherStartStop(t *testing.T) { + wlm := fxutil.Test[workloadmeta.Mock](t, fx.Options( + logimpl.MockModule(), + config.MockModule(), + fx.Supply(context.Background()), + fx.Supply(workloadmeta.NewParams()), + workloadmeta.MockModuleV2(), + )) + pw := newPodWatcher(wlm) + ctx, cancel := context.WithCancel(context.Background()) + go pw.Start(ctx) + pod := &workloadmeta.KubernetesPod{ + EntityID: workloadmeta.EntityID{ + Kind: workloadmeta.KindKubernetesPod, + ID: "p1", + }, + EntityMeta: workloadmeta.EntityMeta{ + Name: "pod1", + Namespace: "default", + }, + Owners: []workloadmeta.KubernetesPodOwner{{Kind: kubernetes.ReplicaSetKind, Name: "deploymentName-766dbb7846"}}, + } + + wlm.Set(pod) + + expectedOwner := NamespacedPodOwner{ + Namespace: "default", + Kind: kubernetes.DeploymentKind, + Name: "deploymentName", + } + + assert.Eventuallyf(t, func() bool { + pods := pw.GetPodsForOwner(expectedOwner) + return pods != nil + }, 5*time.Second, 200*time.Millisecond, "expected pod to be added to the pod watcher") + newPods := pw.GetPodsForOwner(expectedOwner) + require.Len(t, newPods, 1) + assert.Equal(t, pod, newPods[0]) + cancel() +} + +func TestGetNamespacedPodOwner(t *testing.T) { + for _, tt := range []struct { + name string + ns string + owner *workloadmeta.KubernetesPodOwner + expected NamespacedPodOwner + }{ + { + name: "pod owned by deployment", + ns: "default", + owner: &workloadmeta.KubernetesPodOwner{ + Kind: kubernetes.ReplicaSetKind, + Name: "datadog-agent-linux-cluster-agent-f64dd88", + }, + expected: NamespacedPodOwner{ + Namespace: "default", + Kind: kubernetes.DeploymentKind, + Name: "datadog-agent-linux-cluster-agent", + }, + }, + { + name: "pod owned by daemonset", + ns: "default", + owner: &workloadmeta.KubernetesPodOwner{ + Kind: kubernetes.DaemonSetKind, + Name: "datadog-agent-f64dd88", + }, + expected: NamespacedPodOwner{ + Namespace: "default", + Kind: kubernetes.DaemonSetKind, + Name: "datadog-agent-f64dd88", + }, + }, + { + name: "pod owned by replica set", + ns: "default", + owner: &workloadmeta.KubernetesPodOwner{ + Kind: kubernetes.ReplicaSetKind, + Name: "datadog-agent-linux-cluster-agent", + }, + expected: NamespacedPodOwner{ + Namespace: "default", + Kind: kubernetes.ReplicaSetKind, + Name: "datadog-agent-linux-cluster-agent", + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + res := getNamespacedPodOwner(tt.ns, tt.owner) + assert.Equal(t, tt.expected, res) + }) + } +} From 55f2bf18f7373551325268a9ebed3c37c6f71884 Mon Sep 17 00:00:00 2001 From: AliDatadog <125997632+AliDatadog@users.noreply.github.com> Date: Fri, 24 May 2024 11:56:53 +0200 Subject: [PATCH 13/32] [CONTINT-3877] Add admission controller webhook for autoscaling (#25865) Co-authored-by: vboulineau --- .../subcommands/start/command.go | 11 +- .../controllers/webhook/controller_base.go | 20 +- .../webhook/controller_base_test.go | 2 + .../controllers/webhook/controller_v1.go | 14 +- .../controllers/webhook/controller_v1_test.go | 9 +- .../controllers/webhook/controller_v1beta1.go | 5 +- .../webhook/controller_v1beta1_test.go | 3 +- .../mutate/autoscaling/autoscaling.go | 162 ++++++++ .../mutate/autoscaling/autoscaling_test.go | 346 ++++++++++++++++++ .../admission/mutate/autoscaling/telemetry.go | 29 ++ .../admission/mutate/common/common.go | 2 +- pkg/clusteragent/admission/start.go | 4 +- .../autoscaling/workload/patcher.go | 7 +- .../autoscaling/workload/provider.go | 12 +- 14 files changed, 603 insertions(+), 23 deletions(-) create mode 100644 pkg/clusteragent/admission/mutate/autoscaling/autoscaling.go create mode 100644 pkg/clusteragent/admission/mutate/autoscaling/autoscaling_test.go create mode 100644 pkg/clusteragent/admission/mutate/autoscaling/telemetry.go diff --git a/cmd/cluster-agent/subcommands/start/command.go b/cmd/cluster-agent/subcommands/start/command.go index feba5a525f646c..1b0af662470c1f 100644 --- a/cmd/cluster-agent/subcommands/start/command.go +++ b/cmd/cluster-agent/subcommands/start/command.go @@ -399,13 +399,20 @@ func start(log log.Component, } // Autoscaling Product + var pa workload.PatcherAdapter if config.GetBool("autoscaling.workload.enabled") { if rcClient == nil { return fmt.Errorf("Remote config is disabled or failed to initialize, remote config is a required dependency for autoscaling") } - if err := workload.StartWorkloadAutoscaling(mainCtx, apiCl, rcClient); err != nil { + if !config.GetBool("admission_controller.enabled") { + log.Error("Admission controller is disabled, vertical autoscaling requires the admission controller to be enabled. Vertical scaling will be disabled.") + } + + if adapter, err := workload.StartWorkloadAutoscaling(mainCtx, apiCl, rcClient); err != nil { pkglog.Errorf("Error while starting workload autoscaling: %v", err) + } else { + pa = adapter } } @@ -454,7 +461,7 @@ func start(log log.Component, StopCh: stopCh, } - webhooks, err := admissionpkg.StartControllers(admissionCtx, wmeta) + webhooks, err := admissionpkg.StartControllers(admissionCtx, wmeta, pa) if err != nil { pkglog.Errorf("Could not start admission controller: %v", err) } else { diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_base.go b/pkg/clusteragent/admission/controllers/webhook/controller_base.go index 5ece8c92bfeb36..fa450c6062ad15 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_base.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_base.go @@ -26,9 +26,11 @@ import ( "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/metrics" agentsidecar "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/agent_sidecar" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/autoinstrumentation" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/autoscaling" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/config" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/cwsinstrumentation" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/tagsfromlabels" + "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" "github.com/DataDog/datadog-agent/pkg/util/log" ) @@ -39,12 +41,21 @@ type Controller interface { } // NewController returns the adequate implementation of the Controller interface. -func NewController(client kubernetes.Interface, secretInformer coreinformers.SecretInformer, admissionInterface admissionregistration.Interface, isLeaderFunc func() bool, isLeaderNotif <-chan struct{}, config Config, wmeta workloadmeta.Component) Controller { +func NewController( + client kubernetes.Interface, + secretInformer coreinformers.SecretInformer, + admissionInterface admissionregistration.Interface, + isLeaderFunc func() bool, + isLeaderNotif <-chan struct{}, + config Config, + wmeta workloadmeta.Component, + pa workload.PatcherAdapter, +) Controller { if config.useAdmissionV1() { - return NewControllerV1(client, secretInformer, admissionInterface.V1().MutatingWebhookConfigurations(), isLeaderFunc, isLeaderNotif, config, wmeta) + return NewControllerV1(client, secretInformer, admissionInterface.V1().MutatingWebhookConfigurations(), isLeaderFunc, isLeaderNotif, config, wmeta, pa) } - return NewControllerV1beta1(client, secretInformer, admissionInterface.V1beta1().MutatingWebhookConfigurations(), isLeaderFunc, isLeaderNotif, config, wmeta) + return NewControllerV1beta1(client, secretInformer, admissionInterface.V1beta1().MutatingWebhookConfigurations(), isLeaderFunc, isLeaderNotif, config, wmeta, pa) } // MutatingWebhook represents a mutating webhook @@ -74,11 +85,12 @@ type MutatingWebhook interface { // the config one. The reason is that the volume mount for the APM socket added // by the config webhook doesn't always work on Fargate (one of the envs where // we use an agent sidecar), and the agent sidecar webhook needs to remove it. -func mutatingWebhooks(wmeta workloadmeta.Component) []MutatingWebhook { +func mutatingWebhooks(wmeta workloadmeta.Component, pa workload.PatcherAdapter) []MutatingWebhook { webhooks := []MutatingWebhook{ config.NewWebhook(wmeta), tagsfromlabels.NewWebhook(wmeta), agentsidecar.NewWebhook(), + autoscaling.NewWebhook(pa), } apm, err := autoinstrumentation.GetWebhook(wmeta) diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_base_test.go b/pkg/clusteragent/admission/controllers/webhook/controller_base_test.go index d2ec7bfe565dc9..9999a3adff884c 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_base_test.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_base_test.go @@ -34,6 +34,7 @@ func TestNewController(t *testing.T) { make(chan struct{}), v1Cfg, wmeta, + nil, ) assert.IsType(t, &ControllerV1{}, controller) @@ -47,6 +48,7 @@ func TestNewController(t *testing.T) { make(chan struct{}), v1beta1Cfg, wmeta, + nil, ) assert.IsType(t, &ControllerV1beta1{}, controller) diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_v1.go b/pkg/clusteragent/admission/controllers/webhook/controller_v1.go index 49350d3975c658..3babbb96dbd171 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_v1.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_v1.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/util/workqueue" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" + "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/certificate" "github.com/DataDog/datadog-agent/pkg/util/log" ) @@ -39,7 +40,16 @@ type ControllerV1 struct { } // NewControllerV1 returns a new Webhook Controller using admissionregistration/v1. -func NewControllerV1(client kubernetes.Interface, secretInformer coreinformers.SecretInformer, webhookInformer admissioninformers.MutatingWebhookConfigurationInformer, isLeaderFunc func() bool, isLeaderNotif <-chan struct{}, config Config, wmeta workloadmeta.Component) *ControllerV1 { +func NewControllerV1( + client kubernetes.Interface, + secretInformer coreinformers.SecretInformer, + webhookInformer admissioninformers.MutatingWebhookConfigurationInformer, + isLeaderFunc func() bool, + isLeaderNotif <-chan struct{}, + config Config, + wmeta workloadmeta.Component, + pa workload.PatcherAdapter, +) *ControllerV1 { controller := &ControllerV1{} controller.clientSet = client controller.config = config @@ -50,7 +60,7 @@ func NewControllerV1(client kubernetes.Interface, secretInformer coreinformers.S controller.queue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "webhooks") controller.isLeaderFunc = isLeaderFunc controller.isLeaderNotif = isLeaderNotif - controller.mutatingWebhooks = mutatingWebhooks(wmeta) + controller.mutatingWebhooks = mutatingWebhooks(wmeta, pa) controller.generateTemplates() if _, err := secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_v1_test.go b/pkg/clusteragent/admission/controllers/webhook/controller_v1_test.go index 555ac5cf7a68e3..71609a1a3504c5 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_v1_test.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_v1_test.go @@ -34,8 +34,10 @@ import ( "github.com/DataDog/datadog-agent/pkg/util/kubernetes/certificate" ) -const waitFor = 10 * time.Second -const tick = 50 * time.Millisecond +const ( + waitFor = 10 * time.Second + tick = 50 * time.Millisecond +) var v1Cfg = NewConfig(true, false) @@ -952,7 +954,7 @@ func TestGenerateTemplatesV1(t *testing.T) { c := &ControllerV1{} c.config = tt.configFunc() - c.mutatingWebhooks = mutatingWebhooks(wmeta) + c.mutatingWebhooks = mutatingWebhooks(wmeta, nil) c.generateTemplates() assert.EqualValues(t, tt.want(), c.webhookTemplates) @@ -1087,6 +1089,7 @@ func (f *fixtureV1) createController() (*ControllerV1, informers.SharedInformerF make(chan struct{}), v1Cfg, wmeta, + nil, ), factory } diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1.go b/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1.go index bfa6ff17b85a62..0643f600e9664f 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1.go @@ -25,6 +25,7 @@ import ( "k8s.io/client-go/util/workqueue" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" + "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/certificate" "github.com/DataDog/datadog-agent/pkg/util/log" ) @@ -39,7 +40,7 @@ type ControllerV1beta1 struct { } // NewControllerV1beta1 returns a new Webhook Controller using admissionregistration/v1beta1. -func NewControllerV1beta1(client kubernetes.Interface, secretInformer coreinformers.SecretInformer, webhookInformer admissioninformers.MutatingWebhookConfigurationInformer, isLeaderFunc func() bool, isLeaderNotif <-chan struct{}, config Config, wmeta workloadmeta.Component) *ControllerV1beta1 { +func NewControllerV1beta1(client kubernetes.Interface, secretInformer coreinformers.SecretInformer, webhookInformer admissioninformers.MutatingWebhookConfigurationInformer, isLeaderFunc func() bool, isLeaderNotif <-chan struct{}, config Config, wmeta workloadmeta.Component, pa workload.PatcherAdapter) *ControllerV1beta1 { controller := &ControllerV1beta1{} controller.clientSet = client controller.config = config @@ -50,7 +51,7 @@ func NewControllerV1beta1(client kubernetes.Interface, secretInformer coreinform controller.queue = workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "webhooks") controller.isLeaderFunc = isLeaderFunc controller.isLeaderNotif = isLeaderNotif - controller.mutatingWebhooks = mutatingWebhooks(wmeta) + controller.mutatingWebhooks = mutatingWebhooks(wmeta, pa) controller.generateTemplates() if _, err := secretInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ diff --git a/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1_test.go b/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1_test.go index e7ff5307560f0d..df425e2f98d72f 100644 --- a/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1_test.go +++ b/pkg/clusteragent/admission/controllers/webhook/controller_v1beta1_test.go @@ -947,7 +947,7 @@ func TestGenerateTemplatesV1beta1(t *testing.T) { c := &ControllerV1beta1{} c.config = tt.configFunc() - c.mutatingWebhooks = mutatingWebhooks(wmeta) + c.mutatingWebhooks = mutatingWebhooks(wmeta, nil) c.generateTemplates() assert.EqualValues(t, tt.want(), c.webhookTemplates) @@ -1082,6 +1082,7 @@ func (f *fixtureV1beta1) createController() (*ControllerV1beta1, informers.Share make(chan struct{}), v1beta1Cfg, wmeta, + nil, ), factory } diff --git a/pkg/clusteragent/admission/mutate/autoscaling/autoscaling.go b/pkg/clusteragent/admission/mutate/autoscaling/autoscaling.go new file mode 100644 index 00000000000000..f5e03542db185e --- /dev/null +++ b/pkg/clusteragent/admission/mutate/autoscaling/autoscaling.go @@ -0,0 +1,162 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +//go:build kubeapiserver + +// Package autoscaling implements the webhook that vertically scales applications +package autoscaling + +import ( + "fmt" + + "github.com/DataDog/datadog-agent/cmd/cluster-agent/admission" + "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/mutate/common" + "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" + "github.com/DataDog/datadog-agent/pkg/config" + "github.com/DataDog/datadog-agent/pkg/util/kubernetes" + + admiv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +const ( + recommendationIDAnotation = "autoscaling.datadoghq.com/rec-id" + + webhookName = "autoscaling" + webhookEndpoint = "/autoscaling" + + requestType = "request" + limitType = "limit" +) + +// Webhook implements the MutatingWebhook interface +type Webhook struct { + name string + isEnabled bool + endpoint string + resources []string + operations []admiv1.OperationType + recommender workload.PatcherAdapter +} + +// NewWebhook returns a new Webhook +func NewWebhook(recommender workload.PatcherAdapter) *Webhook { + return &Webhook{ + name: webhookName, + isEnabled: config.Datadog.GetBool("autoscaling.workload.enabled"), + endpoint: webhookEndpoint, + resources: []string{"pods"}, + operations: []admiv1.OperationType{admiv1.Create}, + recommender: recommender, + } +} + +// Name returns the name of the webhook +func (w *Webhook) Name() string { + return w.name +} + +// IsEnabled returns whether the webhook is enabled +func (w *Webhook) IsEnabled() bool { + return w.isEnabled +} + +// Endpoint returns the endpoint of the webhook +func (w *Webhook) Endpoint() string { + return w.endpoint +} + +// Resources returns the kubernetes resources for which the webhook should +// be invoked +func (w *Webhook) Resources() []string { + return w.resources +} + +// Operations returns the operations on the resources specified for which +// the webhook should be invoked +func (w *Webhook) Operations() []admiv1.OperationType { + return w.operations +} + +// LabelSelectors returns the label selectors that specify when the webhook +// should be invoked +func (w *Webhook) LabelSelectors(_ bool) (namespaceSelector *metav1.LabelSelector, objectSelector *metav1.LabelSelector) { + // Autoscaling does not work like others. Targets are selected through existence of DPA objects. + // Hence, we need the equivalent of mutate unlabelled for this webhook. + return nil, nil +} + +// MutateFunc returns the function that mutates the resources +func (w *Webhook) MutateFunc() admission.WebhookFunc { + return w.mutate +} + +// mutate adds the DD_AGENT_HOST and DD_ENTITY_ID env vars to the pod template if they don't exist +func (w *Webhook) mutate(request *admission.MutateRequest) ([]byte, error) { + return common.Mutate(request.Raw, request.Namespace, w.Name(), w.updateResources, request.DynamicClient) +} + +// updateResource finds the owner of a pod, calls the recommender to retrieve the recommended CPU and Memory +// requests +func (w *Webhook) updateResources(pod *corev1.Pod, ns string, _ dynamic.Interface) (bool, error) { + if len(pod.OwnerReferences) == 0 { + return false, fmt.Errorf("no owner found for pod %s", pod.Name) + } + ownerRef := pod.OwnerReferences[0] + if ownerRef.Kind == kubernetes.ReplicaSetKind { + ownerRef.Kind = kubernetes.DeploymentKind + ownerRef.Name = kubernetes.ParseDeploymentForReplicaSet(ownerRef.Name) + } + // ParseDeploymentForReplicaSet returns "" when the parsing fails + if ownerRef.Name == "" { + return false, fmt.Errorf("no owner found for pod %s", pod.Name) + } + + recommendationID, recommendations, err := w.recommender.GetRecommendations(pod.Namespace, ownerRef) + if err != nil || recommendationID == "" { + return false, err + } + + // Patching the pod with the recommendations + injected := false + if pod.Annotations[recommendationIDAnotation] != recommendationID { + pod.Annotations[recommendationIDAnotation] = recommendationID + injected = true + } + + for _, reco := range recommendations { + for i := range pod.Spec.Containers { + cont := &pod.Spec.Containers[i] + if cont.Name != reco.Name { + continue + } + if cont.Resources.Limits == nil { + cont.Resources.Limits = corev1.ResourceList{} + } + if cont.Resources.Requests == nil { + cont.Resources.Requests = corev1.ResourceList{} + } + for resource, limit := range reco.Limits { + if limit != cont.Resources.Limits[resource] { + cont.Resources.Limits[resource] = limit + injections.Set(limit.AsApproximateFloat64(), string(resource), ns, ownerRef.Name, cont.Name, limitType, recommendationID) + injected = true + } + } + for resource, request := range reco.Requests { + if request != cont.Resources.Requests[resource] { + cont.Resources.Requests[resource] = request + injections.Set(request.AsApproximateFloat64(), string(resource), ns, ownerRef.Name, cont.Name, requestType, recommendationID) + injected = true + } + } + break + } + } + + return injected, nil +} diff --git a/pkg/clusteragent/admission/mutate/autoscaling/autoscaling_test.go b/pkg/clusteragent/admission/mutate/autoscaling/autoscaling_test.go new file mode 100644 index 00000000000000..c5931ceeacd8bd --- /dev/null +++ b/pkg/clusteragent/admission/mutate/autoscaling/autoscaling_test.go @@ -0,0 +1,346 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +//go:build test && kubeapiserver + +package autoscaling + +import ( + "fmt" + "reflect" + "testing" + + datadoghq "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/dynamic" +) + +type fakeRecommender struct { + recID string + recommendations map[string][]datadoghq.DatadogPodAutoscalerContainerResources + err error +} + +// GetRecommendations returns recommendations +func (f *fakeRecommender) GetRecommendations(_ string, ownerRef metav1.OwnerReference) (string, []datadoghq.DatadogPodAutoscalerContainerResources, error) { + if recs, ok := f.recommendations[ownerRef.Name]; ok { + return f.recID, recs, nil + } + + return "", nil, f.err +} + +func TestUpdateResources(t *testing.T) { + tests := []struct { + name string + wh *Webhook + pod corev1.Pod + wantInjected bool + wantErr bool + wantPod corev1.Pod + }{ + { + name: "update resources when recommendations differ", + wh: &Webhook{ + recommender: &fakeRecommender{ + recID: "version1", + recommendations: map[string][]datadoghq.DatadogPodAutoscalerContainerResources{ + "test-deployment": { + {Name: "container1", Limits: corev1.ResourceList{"cpu": resource.MustParse("500m")}, Requests: corev1.ResourceList{"memory": resource.MustParse("256Mi")}}, + }, + }, + }, + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}, + Annotations: map[string]string{recommendationIDAnotation: "version0"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + wantInjected: true, + wantPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}, + Annotations: map[string]string{recommendationIDAnotation: "version1"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("500m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("256Mi")}, + }, + }}, + }, + }, + }, + { + name: "update resources when there are none", + wh: &Webhook{ + recommender: &fakeRecommender{ + recID: "version0", + recommendations: map[string][]datadoghq.DatadogPodAutoscalerContainerResources{ + "test-deployment": { + {Name: "container1", Limits: corev1.ResourceList{"cpu": resource.MustParse("500m")}, Requests: corev1.ResourceList{"memory": resource.MustParse("256Mi")}}, + }, + }, + }, + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}, + Annotations: map[string]string{recommendationIDAnotation: "version0"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + }}, + }, + }, + wantInjected: true, + wantPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}, + Annotations: map[string]string{recommendationIDAnotation: "version0"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("500m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("256Mi")}, + }, + }}, + }, + }, + }, + { + name: "no update when recommendations match", + wh: &Webhook{ + recommender: &fakeRecommender{ + recID: "version0", + recommendations: map[string][]datadoghq.DatadogPodAutoscalerContainerResources{ + "test-deployment": { + {Name: "container1", Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}}, + }, + }, + }, + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}, + Annotations: map[string]string{recommendationIDAnotation: "version0"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + wantPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}, + Annotations: map[string]string{recommendationIDAnotation: "version0"}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + }, + { + name: "no update when pod has no owner", + wh: &Webhook{}, + pod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + wantPod: corev1.Pod{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + wantErr: true, + }, + { + name: "no update when deployment can't be parsed", + wh: &Webhook{}, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-notahash", + }}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + wantPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-notahash", + }}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + wantErr: true, + }, + { + name: "no update on errors", + wh: &Webhook{ + recommender: &fakeRecommender{ + err: fmt.Errorf("error"), + }, + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + wantErr: true, + wantPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + }, + { + name: "no update on empty recommendations", + wh: &Webhook{ + recommender: &fakeRecommender{}, + }, + pod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + wantErr: false, + wantPod: corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{OwnerReferences: []metav1.OwnerReference{{ + Kind: "ReplicaSet", + Name: "test-deployment-968f49d86", + }}}, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "container1", + Resources: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{"cpu": resource.MustParse("200m")}, + Requests: corev1.ResourceList{"memory": resource.MustParse("128Mi")}, + }, + }}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + injected, err := tt.wh.updateResources(&tt.pod, tt.pod.Namespace, dynamic.Interface(nil)) + if (err != nil) != tt.wantErr { + t.Errorf("updateResources() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.wantInjected, injected) + if !reflect.DeepEqual(tt.pod, tt.wantPod) { + t.Errorf("updateResources() pod = %v, wantPod %v", tt.pod, tt.wantPod) + } + }) + } +} diff --git a/pkg/clusteragent/admission/mutate/autoscaling/telemetry.go b/pkg/clusteragent/admission/mutate/autoscaling/telemetry.go new file mode 100644 index 00000000000000..65a01d9aa43b0c --- /dev/null +++ b/pkg/clusteragent/admission/mutate/autoscaling/telemetry.go @@ -0,0 +1,29 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +//go:build kubeapiserver + +package autoscaling + +import "github.com/DataDog/datadog-agent/pkg/telemetry" + +const subsystem = "autoscaling_webhook" + +var commonOpts = telemetry.Options{NoDoubleUnderscoreSep: true} + +var injections = telemetry.NewGaugeWithOpts( + subsystem, + "resource_injection", + []string{ + "resource", // CPU / Memory + "namespace", + "owner", // Kube deployment / statefulset + "container", // Container name + "type", // Request / Limit + "rec_id", // Hash of the recommendations + }, + "The injections performed on a specific workload", + commonOpts, +) diff --git a/pkg/clusteragent/admission/mutate/common/common.go b/pkg/clusteragent/admission/mutate/common/common.go index c3d067c8c66842..ce3a499320edfc 100644 --- a/pkg/clusteragent/admission/mutate/common/common.go +++ b/pkg/clusteragent/admission/mutate/common/common.go @@ -24,7 +24,7 @@ import ( ) // MutationFunc is a function that mutates a pod -type MutationFunc func(*corev1.Pod, string, dynamic.Interface) (bool, error) +type MutationFunc func(pod *corev1.Pod, ns string, cl dynamic.Interface) (bool, error) // Mutate handles mutating pods and encoding and decoding admission // requests and responses for the public mutate functions diff --git a/pkg/clusteragent/admission/start.go b/pkg/clusteragent/admission/start.go index a172cfae9c00f8..2f42590ece0dbe 100644 --- a/pkg/clusteragent/admission/start.go +++ b/pkg/clusteragent/admission/start.go @@ -14,6 +14,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/controllers/secret" "github.com/DataDog/datadog-agent/pkg/clusteragent/admission/controllers/webhook" + "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload" "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/apiserver" "github.com/DataDog/datadog-agent/pkg/util/kubernetes/apiserver/common" @@ -35,7 +36,7 @@ type ControllerContext struct { } // StartControllers starts the secret and webhook controllers -func StartControllers(ctx ControllerContext, wmeta workloadmeta.Component) ([]webhook.MutatingWebhook, error) { +func StartControllers(ctx ControllerContext, wmeta workloadmeta.Component, pa workload.PatcherAdapter) ([]webhook.MutatingWebhook, error) { if !config.Datadog.GetBool("admission_controller.enabled") { log.Info("Admission controller is disabled") return nil, nil @@ -76,6 +77,7 @@ func StartControllers(ctx ControllerContext, wmeta workloadmeta.Component) ([]we ctx.LeaderSubscribeFunc(), webhookConfig, wmeta, + pa, ) go secretController.Run(ctx.StopCh) diff --git a/pkg/clusteragent/autoscaling/workload/patcher.go b/pkg/clusteragent/autoscaling/workload/patcher.go index 8ae3ca7cba5f7d..2417776715b98c 100644 --- a/pkg/clusteragent/autoscaling/workload/patcher.go +++ b/pkg/clusteragent/autoscaling/workload/patcher.go @@ -10,9 +10,10 @@ package workload import ( "fmt" - "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload/model" datadoghq "github.com/DataDog/datadog-operator/apis/datadoghq/v1alpha1" + "github.com/DataDog/datadog-agent/pkg/clusteragent/autoscaling/workload/model" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -29,6 +30,10 @@ type patcherAdapter struct { var _ PatcherAdapter = patcherAdapter{} +func newPatcherAdapter(store *store) PatcherAdapter { + return patcherAdapter{store: store} +} + // GetPodAutoscalerFromOwnerRef searches for a PodAutoscalerInternal object associated with the given owner reference // If no PodAutoscalerInternal is found or no vertical recommendation, it returns ("", nil, nil) func (pa patcherAdapter) GetRecommendations(ns string, ownerRef metav1.OwnerReference) (string, []datadoghq.DatadogPodAutoscalerContainerResources, error) { diff --git a/pkg/clusteragent/autoscaling/workload/provider.go b/pkg/clusteragent/autoscaling/workload/provider.go index 73854eef021b99..060f335a84f2b7 100644 --- a/pkg/clusteragent/autoscaling/workload/provider.go +++ b/pkg/clusteragent/autoscaling/workload/provider.go @@ -18,26 +18,26 @@ import ( ) // StartWorkloadAutoscaling starts the workload autoscaling controller -func StartWorkloadAutoscaling(ctx context.Context, apiCl *apiserver.APIClient, rcClient rcClient) error { +func StartWorkloadAutoscaling(ctx context.Context, apiCl *apiserver.APIClient, rcClient rcClient) (PatcherAdapter, error) { if apiCl == nil { - return fmt.Errorf("Impossible to start workload autoscaling without valid APIClient") + return nil, fmt.Errorf("Impossible to start workload autoscaling without valid APIClient") } le, err := leaderelection.GetLeaderEngine() if err != nil { - return fmt.Errorf("Unable to start workload autoscaling as LeaderElection failed with: %v", err) + return nil, fmt.Errorf("Unable to start workload autoscaling as LeaderElection failed with: %v", err) } store := autoscaling.NewStore[model.PodAutoscalerInternal]() _, err = newConfigRetriever(store, le.IsLeader, rcClient) if err != nil { - return fmt.Errorf("Unable to start workload autoscaling config retriever: %w", err) + return nil, fmt.Errorf("Unable to start workload autoscaling config retriever: %w", err) } controller, err := newController(apiCl.RESTMapper, apiCl.ScaleCl, apiCl.DynamicInformerCl, apiCl.DynamicInformerFactory, le.IsLeader, store) if err != nil { - return fmt.Errorf("Unable to start workload autoscaling controller: %w", err) + return nil, fmt.Errorf("Unable to start workload autoscaling controller: %w", err) } // Start informers & controllers (informers can be started multiple times) @@ -46,5 +46,5 @@ func StartWorkloadAutoscaling(ctx context.Context, apiCl *apiserver.APIClient, r go controller.Run(ctx) - return nil + return newPatcherAdapter(store), nil } From d89b0de633a6a5cad936e9c74ffe8678ef826adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Juli=C3=A1n?= Date: Fri, 24 May 2024 12:08:22 +0200 Subject: [PATCH 14/32] [EBPF] Add kmt.selftest task (#25536) * Add selftest task to KMT * Document selftest * Fix linter * Fix linter * PR fixes * Fix linting * Fix typing --- tasks/kernel_matrix_testing/selftest.py | 132 ++++++++++++++++++++++++ tasks/kernel_matrix_testing/tool.py | 4 +- tasks/kernel_matrix_testing/types.py | 4 +- tasks/kernel_matrix_testing/vars.py | 43 ++++++++ tasks/kernel_matrix_testing/vmconfig.py | 15 +-- tasks/kmt.py | 70 +++++-------- 6 files changed, 206 insertions(+), 62 deletions(-) create mode 100644 tasks/kernel_matrix_testing/selftest.py diff --git a/tasks/kernel_matrix_testing/selftest.py b/tasks/kernel_matrix_testing/selftest.py new file mode 100644 index 00000000000000..c8872404b3a020 --- /dev/null +++ b/tasks/kernel_matrix_testing/selftest.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import functools +import re + +from invoke.context import Context + +from tasks.kernel_matrix_testing.platforms import get_platforms +from tasks.kernel_matrix_testing.tool import error, full_arch, get_binary_target_arch, info, warn +from tasks.kernel_matrix_testing.types import Component +from tasks.kernel_matrix_testing.vars import KMTPaths, arch_ls + +SelftestResult = tuple[bool | None, str] + + +def selftest_pulumi(ctx: Context, _: bool) -> SelftestResult: + """Tests that pulumi is installed and can be run.""" + res = ctx.run("pulumi --non-interactive plugin ls", hide=True, warn=True) + if res is None or not res.ok: + return False, "Cannot run pulumi, check installation" + return True, "pulumi is installed" + + +def selftest_platforms_json(ctx: Context, _: bool) -> SelftestResult: + """Checks that platforms.json file is readable and correct.""" + try: + plat = get_platforms() + except Exception as e: + return False, f"Cannot read platforms.json file: {e}" + + image_vers: set[str] = set() + for arch in arch_ls: + if arch not in plat: + return False, f"Missing {arch} in platforms.json file" + + for image, data in plat[arch].items(): + if "image_version" not in data: + return False, f"Image {image} does not have image_version field" + image_vers.add(data["image_version"]) + + if len(image_vers) != 1: + return False, f"Multiple image versions found: {image_vers}" + + res = ctx.run("inv -e kmt.ls", hide=True, warn=True) + if res is None or not res.ok: + return False, "Cannot run inv -e kmt.ls, platforms.json file might be incorrect" + return True, "platforms.json file exists, is readable and is correct" + + +def selftest_prepare(ctx: Context, allow_infra_changes: bool, component: Component) -> SelftestResult: + """Ensures that we can run kmt.prepare for a given component. + + If allow_infra_changes is true, the stack will be created if it doesn't exist. + """ + stack = "selftest-prepare" + arch = full_arch("local") + vms = f"{arch}-debian11-distro" + + ctx.run(f"inv kmt.destroy-stack --stack={stack}", warn=True, hide=True) + res = ctx.run(f"inv -e kmt.gen-config --stack={stack} --vms={vms} --init-stack --yes", warn=True) + if res is None or not res.ok: + return None, "Cannot generate config with inv kmt.gen-config" + + if allow_infra_changes: + res = ctx.run(f"inv kmt.launch-stack --stack={stack}", warn=True) + if res is None or not res.ok: + return None, "Cannot create stack with inv kmt.create-stack" + + compile_only = "--compile-only" if not allow_infra_changes else "" + res = ctx.run(f"inv -e kmt.prepare --stack={stack} --component={component} {compile_only}", warn=True) + if res is None or not res.ok: + return False, "Cannot run inv -e kmt.prepare" + + paths = KMTPaths(stack, arch) + testpath = paths.secagent_tests if component == "security-agent" else paths.sysprobe_tests + if not testpath.is_dir(): + return False, f"Tests directory {testpath} not found" + + bytecode_dir = testpath / "pkg" / "ebpf" / "bytecode" / "build" + object_files = list(bytecode_dir.glob("*.o")) + if len(object_files) == 0: + return False, f"No object files found in {bytecode_dir}" + + if component == "security-agent": + test_binary = testpath / "pkg" / "security" / "testsuite" + else: + test_binary = testpath / "pkg" / "ebpf" / "testsuite" + + if not test_binary.is_file(): + return False, f"Test binary {test_binary} not found" + + binary_arch = get_binary_target_arch(ctx, test_binary) + if binary_arch != arch: + return False, f"Binary {test_binary} has unexpected arch {binary_arch} instead of {arch}" + + return True, f"inv -e kmt.prepare ran successfully for {component}" + + +def selftest(ctx: Context, allow_infra_changes: bool = False, filter: str | None = None): + """Run all defined selftests + + :param allow_infra_changes: If true, the selftests will create the stack if it doesn't exist + :param filter: If set, only run selftests that match the regex filter + """ + all_selftests = [ + ("pulumi", selftest_pulumi), + ("platforms.json", selftest_platforms_json), + ("sysprobe-prepare", functools.partial(selftest_prepare, component="system-probe")), + ("secagent-prepare", functools.partial(selftest_prepare, component="security-agent")), + ] + results: list[tuple[str, SelftestResult]] = [] + + for name, selftest in all_selftests: + if filter is not None and not re.search(filter, name): + warn(f"[!] Skipping {name}") + continue + + info(f"[+] Running selftest {name}") + try: + results.append((name, selftest(ctx, allow_infra_changes))) + except Exception as e: + results.append((name, (None, f"Exception: {e}"))) + + print("\nSelftest results:") + + for name, (ok, msg) in results: + if ok is None: + warn(f"[!] {name} couldn't complete: {msg}") + elif ok: + info(f"[*] {name} OK: {msg}") + else: + error(f"[!] {name} failed: {msg}") diff --git a/tasks/kernel_matrix_testing/tool.py b/tasks/kernel_matrix_testing/tool.py index 9b9c74428fce94..a6d332873f5f8e 100644 --- a/tasks/kernel_matrix_testing/tool.py +++ b/tasks/kernel_matrix_testing/tool.py @@ -10,7 +10,7 @@ from tasks.kernel_matrix_testing.vars import arch_mapping if TYPE_CHECKING: - from tasks.kernel_matrix_testing.types import Arch + from tasks.kernel_matrix_testing.types import Arch, PathOrStr try: from termcolor import colored @@ -60,7 +60,7 @@ def full_arch(arch: str): return arch_mapping[arch] -def get_binary_target_arch(ctx: Context, file: str) -> Arch | None: +def get_binary_target_arch(ctx: Context, file: PathOrStr) -> Arch | None: res = ctx.run(f"file {file}") if res is None or not res.ok: return None diff --git a/tasks/kernel_matrix_testing/types.py b/tasks/kernel_matrix_testing/types.py index 5074a3b12e1755..58c41478216606 100644 --- a/tasks/kernel_matrix_testing/types.py +++ b/tasks/kernel_matrix_testing/types.py @@ -6,9 +6,7 @@ from __future__ import annotations import os -from typing import Literal, TypeVar - -from typing_extensions import Protocol, TypedDict +from typing import Literal, Protocol, TypedDict, TypeVar Arch = Literal['x86_64', 'arm64'] ArchOrLocal = Arch | Literal['local'] diff --git a/tasks/kernel_matrix_testing/vars.py b/tasks/kernel_matrix_testing/vars.py index 81a95d44eecee1..8ad518889e7da9 100644 --- a/tasks/kernel_matrix_testing/vars.py +++ b/tasks/kernel_matrix_testing/vars.py @@ -1,5 +1,6 @@ from __future__ import annotations +from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -18,3 +19,45 @@ arch_ls: list[Arch] = ["x86_64", "arm64"] VMCONFIG = "vmconfig.json" + + +class KMTPaths: + def __init__(self, stack: str | None, arch: Arch): + self.stack = stack + self.arch = arch + + @property + def repo_root(self): + # this file is tasks/kernel_matrix_testing/vars.py, so two parents is the agent folder + return Path(__file__).parent.parent.parent + + @property + def root(self): + return self.repo_root / "kmt-deps" + + @property + def arch_dir(self): + return self.stack_dir / self.arch + + @property + def stack_dir(self): + if self.stack is None: + raise RuntimeError("no stack name provided, cannot use stack-specific paths") + + return self.root / self.stack + + @property + def dependencies(self): + return self.arch_dir / "opt/testing-tools" + + @property + def sysprobe_tests(self): + return self.arch_dir / "opt/system-probe-tests" + + @property + def secagent_tests(self): + return self.arch_dir / "opt/security-agent-tests" + + @property + def tools(self): + return self.root / self.arch / "tools" diff --git a/tasks/kernel_matrix_testing/vmconfig.py b/tasks/kernel_matrix_testing/vmconfig.py index c84b90e50214f6..3b9c2f1b316920 100644 --- a/tasks/kernel_matrix_testing/vmconfig.py +++ b/tasks/kernel_matrix_testing/vmconfig.py @@ -643,6 +643,7 @@ def gen_config_for_stack( new: bool, ci: bool, template: str, + yes=False, ): stack = check_and_get_stack(stack) if not stack_exists(stack) and not init_stack: @@ -677,7 +678,7 @@ def gen_config_for_stack( else: ctx.run(f"git diff {vmconfig_file} {tmpfile}", warn=True) - if ask("are you sure you want to apply the diff? (y/n)") != "y": + if not yes and ask("are you sure you want to apply the diff? (y/n)") != "y": warn("[-] diff not applied") return @@ -713,6 +714,7 @@ def gen_config( arch: str, output_file: PathOrStr, template: Component, + yes: bool = False, ): vcpu_ls = vcpu.split(',') memory_ls = memory.split(',') @@ -724,16 +726,7 @@ def gen_config( if not ci: return gen_config_for_stack( - ctx, - stack, - vms, - set_ls, - init_stack, - ls_to_int(vcpu_ls), - ls_to_int(memory_ls), - new, - ci, - template, + ctx, stack, vms, set_ls, init_stack, ls_to_int(vcpu_ls), ls_to_int(memory_ls), new, ci, template, yes=yes ) arch_ls: list[Arch] = ["x86_64", "arm64"] diff --git a/tasks/kmt.py b/tasks/kmt.py index 54a954e4eae12b..e6270e385e358d 100644 --- a/tasks/kmt.py +++ b/tasks/kmt.py @@ -14,6 +14,7 @@ from invoke.context import Context from invoke.tasks import task +from tasks.kernel_matrix_testing import selftest as selftests from tasks.kernel_matrix_testing import stacks, vmconfig from tasks.kernel_matrix_testing.ci import KMTTestRunJob, get_all_jobs_for_pipeline from tasks.kernel_matrix_testing.compiler import CONTAINER_AGENT_PATH, all_compilers, get_compiler @@ -34,7 +35,7 @@ from tasks.kernel_matrix_testing.platforms import get_platforms, platforms_file from tasks.kernel_matrix_testing.stacks import check_and_get_stack, ec2_instance_ids from tasks.kernel_matrix_testing.tool import Exit, ask, error, get_binary_target_arch, info, warn -from tasks.kernel_matrix_testing.vars import arch_ls +from tasks.kernel_matrix_testing.vars import KMTPaths, arch_ls from tasks.libs.build.ninja import NinjaWriter from tasks.libs.common.utils import get_build_flags from tasks.security_agent import build_functional_tests, build_stress_tests @@ -102,6 +103,7 @@ def create_stack(ctx, stack=None): "from-ci-pipeline": "Generate a vmconfig.json file with the VMs that failed jobs in pipeline with the given ID.", "use-local-if-possible": "(Only when --from-ci-pipeline is used) If the VM is for the same architecture as the host, use the local VM instead of the remote one.", "vmconfig_template": "Template to use for the generated vmconfig.json file. Defaults to 'system-probe'. A file named 'vmconfig-.json' must exist in 'tasks/new-e2e/system-probe/config/'", + "yes": "Do not ask for confirmation", } ) def gen_config( @@ -119,6 +121,7 @@ def gen_config( from_ci_pipeline: str | None = None, use_local_if_possible=False, vmconfig_template: Component = "system-probe", + yes=False, ): """ Generate a vmconfig.json file with the given VMs. @@ -137,12 +140,13 @@ def gen_config( output_file=output_file, use_local_if_possible=use_local_if_possible, vmconfig_template=vmconfig_template, + yes=yes, ) else: vcpu = DEFAULT_VCPU if vcpu is None else vcpu memory = DEFAULT_MEMORY if memory is None else memory vmconfig.gen_config( - ctx, stack, vms, sets, init_stack, vcpu, memory, new, ci, arch, output_file, vmconfig_template + ctx, stack, vms, sets, init_stack, vcpu, memory, new, ci, arch, output_file, vmconfig_template, yes=yes ) @@ -159,6 +163,7 @@ def gen_config_from_ci_pipeline( arch: str = "", output_file="vmconfig.json", vmconfig_template: Component = "system-probe", + yes=False, ): """ Generate a vmconfig.json file with the VMs that failed jobs in the given pipeline. @@ -201,7 +206,7 @@ def gen_config_from_ci_pipeline( vcpu = DEFAULT_VCPU if vcpu is None else vcpu memory = DEFAULT_MEMORY if memory is None else memory vmconfig.gen_config( - ctx, stack, ",".join(vms), "", init_stack, vcpu, memory, new, ci, arch, output_file, vmconfig_template + ctx, stack, ",".join(vms), "", init_stack, vcpu, memory, new, ci, arch, output_file, vmconfig_template, yes=yes ) info("[+] You can run the following command to execute only packages with failed tests") print(f"inv kmt.test --packages=\"{' '.join(failed_packages)}\"") @@ -408,48 +413,6 @@ def download_gotestsum(ctx: Context, arch: Arch, fgotestsum: PathOrStr): ctx.run(f"cp {paths.tools}/gotestsum {fgotestsum}") -class KMTPaths: - def __init__(self, stack: str | None, arch: Arch): - self.stack = stack - self.arch = arch - - @property - def repo_root(self): - # this file is tasks/kmt.py, so two parents is the agent folder - return Path(__file__).parent.parent - - @property - def root(self): - return self.repo_root / "kmt-deps" - - @property - def arch_dir(self): - return self.stack_dir / self.arch - - @property - def stack_dir(self): - if self.stack is None: - raise Exit("no stack name provided, cannot use stack-specific paths") - - return self.root / self.stack - - @property - def dependencies(self): - return self.arch_dir / "opt/testing-tools" - - @property - def sysprobe_tests(self): - return self.arch_dir / "opt/system-probe-tests" - - @property - def secagent_tests(self): - return self.arch_dir / "opt/security-agent-tests" - - @property - def tools(self): - return self.root / self.arch / "tools" - - def is_root(): return os.getuid() == 0 @@ -550,6 +513,7 @@ def kmt_secagent_prepare( packages: str | None = None, verbose: bool = True, ci: bool = True, + compile_only: bool = False, ): kmt_paths = KMTPaths(stack, arch) kmt_paths.secagent_tests.mkdir(exist_ok=True, parents=True) @@ -595,6 +559,7 @@ def prepare( packages=None, verbose=True, ci=False, + compile_only=False, ): if not ci: stack = check_and_get_stack(stack) @@ -662,7 +627,7 @@ def prepare( ctx.run(f"mkdir -p {os.path.dirname(df)}") ctx.run(f"install {sf} {df}") - if ci: + if ci or compile_only: return if vms is None or vms == "": @@ -1625,3 +1590,16 @@ def tmux(ctx: Context, stack: str | None = None): ctx.run(f"tmux select-layout -t kmt-{stack_name}:{i} tiled") info(f"[+] Tmux session kmt-{stack_name} created. Attach with 'tmux attach -t kmt-{stack_name}'") + + +@task( + help={ + "allow_infra_changes": "Allow infrastructure changes to be made during the selftest", + "filter": "Filter to run only tests matching the given regex", + } +) +def selftest(ctx: Context, allow_infra_changes=False, filter: str | None = None): + """Run all KMT selftests, reporting status at the end. Can be used for debugging in KMT development + or for troubleshooting. + """ + selftests.selftest(ctx, allow_infra_changes=allow_infra_changes, filter=filter) From 451bf2009225160222f08d38d6b69d247920cc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Momar=20TOUR=C3=89?= <36661127+mftoure@users.noreply.github.com> Date: Fri, 24 May 2024 12:25:49 +0200 Subject: [PATCH 15/32] Fix e2e tests (#25851) * Update downloadPolicies to reflect latest policy download endpoint * remove ioutil * Consider other custom policies files --- .../subcommands/runtime/command.go | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/cmd/security-agent/subcommands/runtime/command.go b/cmd/security-agent/subcommands/runtime/command.go index ae5b752996f7cc..afeaf89a312d77 100644 --- a/cmd/security-agent/subcommands/runtime/command.go +++ b/cmd/security-agent/subcommands/runtime/command.go @@ -9,6 +9,8 @@ package runtime import ( + "archive/zip" + "bytes" "context" "encoding/json" "errors" @@ -17,6 +19,7 @@ import ( "os" "path" "runtime" + "strings" "time" "github.com/spf13/cobra" @@ -186,6 +189,7 @@ type downloadPolicyCliParams struct { check bool outputPath string + source string } func downloadPolicyCommands(globalParams *command.GlobalParams) []*cobra.Command { @@ -210,6 +214,7 @@ func downloadPolicyCommands(globalParams *command.GlobalParams) []*cobra.Command downloadPolicyCmd.Flags().BoolVar(&downloadPolicyArgs.check, "check", false, "Check policies after downloading") downloadPolicyCmd.Flags().StringVar(&downloadPolicyArgs.outputPath, "output-path", "", "Output path for downloaded policies") + downloadPolicyCmd.Flags().StringVar(&downloadPolicyArgs.source, "source", "all", `Specify wether should download the custom, default or all policies. allowed: "all", "default", "custom"`) return []*cobra.Command{downloadPolicyCmd} } @@ -765,7 +770,36 @@ func downloadPolicy(log log.Component, config config.Component, _ secrets.Compon if err != nil { return err } + + // Unzip the downloaded file containing both default and custom policies resBytes := []byte(res) + reader, err := zip.NewReader(bytes.NewReader(resBytes), int64(len(resBytes))) + if err != nil { + return err + } + + var defaultPolicy []byte + var customPolicies []string + + for _, file := range reader.File { + if strings.HasSuffix(file.Name, ".policy") { + pf, err := file.Open() + if err != nil { + return err + } + policyData, err := io.ReadAll(pf) + pf.Close() + if err != nil { + return err + } + + if file.Name == "default.policy" { + defaultPolicy = policyData + } else { + customPolicies = append(customPolicies, string(policyData)) + } + } + } tempDir, err := os.MkdirTemp("", "policy_check") if err != nil { @@ -773,10 +807,14 @@ func downloadPolicy(log log.Component, config config.Component, _ secrets.Compon } defer os.RemoveAll(tempDir) - tempOutputPath := path.Join(tempDir, "check.policy") - if err := os.WriteFile(tempOutputPath, resBytes, 0644); err != nil { + if err := os.WriteFile(path.Join(tempDir, "default.policy"), defaultPolicy, 0644); err != nil { return err } + for i, customPolicy := range customPolicies { + if err := os.WriteFile(path.Join(tempDir, fmt.Sprintf("custom%d.policy", i+1)), []byte(customPolicy), 0644); err != nil { + return err + } + } if downloadPolicyArgs.check { if err := checkPolicies(log, config, &checkPoliciesCliParams{dir: tempDir}); err != nil { @@ -784,7 +822,36 @@ func downloadPolicy(log log.Component, config config.Component, _ secrets.Compon } } - _, err = outputWriter.Write(resBytes) + // Extract and merge rules from custom policies + var customRules string + for _, customPolicy := range customPolicies { + customPolicyLines := strings.Split(customPolicy, "\n") + rulesIndex := -1 + for i, line := range customPolicyLines { + if strings.TrimSpace(line) == "rules:" { + rulesIndex = i + break + } + } + if rulesIndex != -1 && rulesIndex+1 < len(customPolicyLines) { + customRules += "\n" + strings.Join(customPolicyLines[rulesIndex+1:], "\n") + } + } + + // Output depending on user's specification + var outputContent string + switch downloadPolicyArgs.source { + case "all": + outputContent = string(defaultPolicy) + customRules + case "default": + outputContent = string(defaultPolicy) + case "custom": + outputContent = string(customRules) + default: + return errors.New("invalid source specified") + } + + _, err = outputWriter.Write([]byte(outputContent)) if err != nil { return err } From 22b3747908850e4f6530d8ab947f3c39f6d5ff72 Mon Sep 17 00:00:00 2001 From: Guillaume Fournier <36961134+Gui774ume@users.noreply.github.com> Date: Fri, 24 May 2024 13:36:30 +0200 Subject: [PATCH 16/32] [CSM] Disable IMDS from drift events by default (#25884) --- pkg/config/setup/system_probe_cws.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/config/setup/system_probe_cws.go b/pkg/config/setup/system_probe_cws.go index c606da896caf22..06fe9464049ef3 100644 --- a/pkg/config/setup/system_probe_cws.go +++ b/pkg/config/setup/system_probe_cws.go @@ -87,7 +87,7 @@ func initCWSSystemProbeConfig(cfg pkgconfigmodel.Config) { cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.auto_suppression.event_types", []string{"exec", "dns"}) // CWS - Anomaly detection - cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.event_types", []string{"exec", "imds"}) + cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.event_types", []string{"exec"}) cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.default_minimum_stable_period", "900s") cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.minimum_stable_period.exec", "900s") cfg.BindEnvAndSetDefault("runtime_security_config.security_profile.anomaly_detection.minimum_stable_period.dns", "900s") From 249d481102ca245320a00ee1f09c60cfd3a9e6d8 Mon Sep 17 00:00:00 2001 From: Paul Cacheux Date: Fri, 24 May 2024 13:55:06 +0200 Subject: [PATCH 17/32] [CWS] introduce user path for FRIM (#25289) * basic user path resolution * update tests --- pkg/security/probe/field_handlers_windows.go | 5 + .../probe/probe_kernel_file_windows.go | 78 +++++-- pkg/security/probe/probe_windows.go | 56 +++-- pkg/security/secl/model/accessors_windows.go | 215 ++++++++++++++++++ .../secl/model/field_accessors_windows.go | 80 +++++++ .../secl/model/field_handlers_windows.go | 9 + pkg/security/secl/model/model_windows.go | 7 +- pkg/security/tests/file_windows_test.go | 8 +- 8 files changed, 406 insertions(+), 52 deletions(-) diff --git a/pkg/security/probe/field_handlers_windows.go b/pkg/security/probe/field_handlers_windows.go index 2c4fe52e3669f2..c004a4da2093fb 100644 --- a/pkg/security/probe/field_handlers_windows.go +++ b/pkg/security/probe/field_handlers_windows.go @@ -38,6 +38,11 @@ func (fh *FieldHandlers) ResolveFilePath(_ *model.Event, f *model.FileEvent) str return f.PathnameStr } +// ResolveFileUserPath resolves the inode to a full user path +func (fh *FieldHandlers) ResolveFileUserPath(_ *model.Event, f *model.FimFileEvent) string { + return f.UserPathnameStr +} + // ResolveFileBasename resolves the inode to a full path func (fh *FieldHandlers) ResolveFileBasename(_ *model.Event, f *model.FileEvent) string { return f.BasenameStr diff --git a/pkg/security/probe/probe_kernel_file_windows.go b/pkg/security/probe/probe_kernel_file_windows.go index b11f46a7334356..cbc706a6f0c6d6 100644 --- a/pkg/security/probe/probe_kernel_file_windows.go +++ b/pkg/security/probe/probe_kernel_file_windows.go @@ -15,6 +15,7 @@ import ( "github.com/DataDog/datadog-agent/comp/etw" etwimpl "github.com/DataDog/datadog-agent/comp/etw/impl" + "github.com/DataDog/datadog-agent/pkg/security/seclog" "github.com/DataDog/datadog-agent/pkg/util/winutil" "golang.org/x/sys/windows" @@ -79,6 +80,7 @@ type createHandleArgs struct { createAttributes uint32 shareAccess uint32 fileName string + userFileName string } /* @@ -160,6 +162,7 @@ func (wp *WindowsProbe) parseCreateHandleArgs(e *etw.DDEventRecord) (*createHand } else { return nil, fmt.Errorf("unknown version %v", e.EventHeader.EventDescriptor.Version) } + ca.userFileName = wp.mustConvertDrivePath(ca.fileName) if _, ok := wp.discardedPaths.Get(ca.fileName); ok { wp.stats.fileCreateSkippedDiscardedPaths++ @@ -174,7 +177,11 @@ func (wp *WindowsProbe) parseCreateHandleArgs(e *etw.DDEventRecord) (*createHand } // lru is thread safe, has its own locking - if wp.filePathResolver.Add(ca.fileObject, fileCache{fileName: ca.fileName}) { + fc := fileCache{ + fileName: ca.fileName, + userFileName: ca.userFileName, + } + if wp.filePathResolver.Add(ca.fileObject, fc) { wp.stats.fileNameCacheEvictions++ } @@ -233,13 +240,14 @@ func (ca *createNewFileArgs) String() string { // nolint: unused type setInformationArgs struct { etw.DDEventHeader - irp uint64 - threadID uint64 - fileObject fileObjectPointer - fileKey uint64 - extraInfo uint64 - infoClass uint32 - fileName string + irp uint64 + threadID uint64 + fileObject fileObjectPointer + fileKey uint64 + extraInfo uint64 + infoClass uint32 + fileName string + userFileName string } type setDeleteArgs setInformationArgs type renameArgs setInformationArgs @@ -274,6 +282,7 @@ func (wp *WindowsProbe) parseInformationArgs(e *etw.DDEventRecord) (*setInformat // lru is thread safe, has its own locking if s, ok := wp.filePathResolver.Get(fileObjectPointer(sia.fileObject)); ok { sia.fileName = s.fileName + sia.userFileName = s.userFileName } return sia, nil @@ -372,11 +381,12 @@ func (fa *fsctlArgs) String() string { type cleanupArgs struct { etw.DDEventHeader - irp uint64 - threadID uint64 - fileObject fileObjectPointer - fileKey uint64 - fileName string + irp uint64 + threadID uint64 + fileObject fileObjectPointer + fileKey uint64 + fileName string + userFileName string } // nolint: unused @@ -408,6 +418,7 @@ func (wp *WindowsProbe) parseCleanupArgs(e *etw.DDEventRecord) (*cleanupArgs, er // lru is thread safe, has its own locking if s, ok := wp.filePathResolver.Get(fileObjectPointer(ca.fileObject)); ok { ca.fileName = s.fileName + ca.userFileName = s.userFileName } return ca, nil @@ -460,15 +471,16 @@ func (fa *flushArgs) String() string { type readArgs struct { etw.DDEventHeader - ByteOffset uint64 - irp uint64 - threadID uint64 - fileObject fileObjectPointer - fileKey fileObjectPointer - IOSize uint32 - IOFlags uint32 - extraFlags uint32 // zero if version 0, as they're not supplied - fileName string + ByteOffset uint64 + irp uint64 + threadID uint64 + fileObject fileObjectPointer + fileKey fileObjectPointer + IOSize uint32 + IOFlags uint32 + extraFlags uint32 // zero if version 0, as they're not supplied + fileName string + userFileName string } type writeArgs readArgs @@ -500,6 +512,7 @@ func (wp *WindowsProbe) parseReadArgs(e *etw.DDEventRecord) (*readArgs, error) { // lru is thread safe, has its own locking if s, ok := wp.filePathResolver.Get(fileObjectPointer(ra.fileObject)); ok { ra.fileName = s.fileName + ra.userFileName = s.userFileName } return ra, nil } @@ -563,7 +576,9 @@ type deletePathArgs struct { extraInformation uint64 infoClass uint32 filePath string + userFilePath string oldPath string + oldUserPath string } // nolint: unused @@ -594,10 +609,12 @@ func (wp *WindowsProbe) parseDeletePathArgs(e *etw.DDEventRecord) (*deletePathAr dpa.infoClass = data.GetUint32(36) dpa.filePath, _, _, _ = data.ParseUnicodeString(40) } + dpa.userFilePath = wp.mustConvertDrivePath(dpa.filePath) // lru is thread safe, has its own locking if s, ok := wp.filePathResolver.Get(fileObjectPointer(dpa.fileObject)); ok { dpa.oldPath = s.fileName + dpa.oldUserPath = s.userFileName // question, should we reset the filePathResolver here? } return dpa, nil @@ -650,8 +667,9 @@ func (sla *setLinkPath) String() string { type nameCreateArgs struct { etw.DDEventHeader - fileKey fileObjectPointer - fileName string + fileKey fileObjectPointer + fileName string + userFileName string } type nameDeleteArgs nameCreateArgs @@ -670,6 +688,8 @@ func (wp *WindowsProbe) parseNameCreateArgs(e *etw.DDEventRecord) (*nameCreateAr } else { return nil, fmt.Errorf("unknown version number %v", e.EventHeader.EventDescriptor.Version) } + ca.userFileName = wp.mustConvertDrivePath(ca.fileName) + return ca, nil } @@ -713,6 +733,16 @@ func (wp *WindowsProbe) convertDrivePath(devicefilename string) (string, error) } return "", fmt.Errorf("Unable to parse path %v", devicefilename) } + +func (wp *WindowsProbe) mustConvertDrivePath(devicefilename string) string { + userPath, err := wp.convertDrivePath(devicefilename) + if err != nil { + seclog.Errorf("failed to convert drive path: %v", err) + return devicefilename + } + return userPath +} + func (wp *WindowsProbe) initializeVolumeMap() error { buf := make([]uint16, 1024) diff --git a/pkg/security/probe/probe_windows.go b/pkg/security/probe/probe_windows.go index 5e6501f31e8144..5d24b2167209fa 100644 --- a/pkg/security/probe/probe_windows.go +++ b/pkg/security/probe/probe_windows.go @@ -76,7 +76,7 @@ type WindowsProbe struct { regPathResolver *lru.Cache[regObjectPointer, string] // state tracking - renamePreArgs *lru.Cache[uint64, string] + renamePreArgs *lru.Cache[uint64, fileCache] // stats stats stats @@ -99,7 +99,8 @@ type WindowsProbe struct { // filecache currently only has a filename. But this is going to expand really soon. so go ahead // and have the wrapper struct even though right now it doesn't add anything. type fileCache struct { - fileName string + fileName string + userFileName string } type etwNotification struct { @@ -592,17 +593,26 @@ func (p *WindowsProbe) handleETWNotification(ev *model.Event, notif etwNotificat ev.Type = uint32(model.CreateNewFileEventType) ev.CreateNewFile = model.CreateNewFileEvent{ File: model.FimFileEvent{ - FileObject: uint64(arg.fileObject), - PathnameStr: arg.fileName, - BasenameStr: filepath.Base(arg.fileName), + FileObject: uint64(arg.fileObject), + PathnameStr: arg.fileName, + UserPathnameStr: arg.userFileName, + BasenameStr: filepath.Base(arg.fileName), }, } case *renameArgs: - p.renamePreArgs.Add(uint64(arg.fileObject), arg.fileName) + fc := fileCache{ + fileName: arg.fileName, + userFileName: arg.userFileName, + } + p.renamePreArgs.Add(uint64(arg.fileObject), fc) case *rename29Args: - p.renamePreArgs.Add(uint64(arg.fileObject), arg.fileName) + fc := fileCache{ + fileName: arg.fileName, + userFileName: arg.userFileName, + } + p.renamePreArgs.Add(uint64(arg.fileObject), fc) case *renamePath: - path, found := p.renamePreArgs.Get(uint64(arg.fileObject)) + fileCache, found := p.renamePreArgs.Get(uint64(arg.fileObject)) if !found { log.Debugf("unable to find renamePreArgs for %d", uint64(arg.fileObject)) return @@ -610,14 +620,16 @@ func (p *WindowsProbe) handleETWNotification(ev *model.Event, notif etwNotificat ev.Type = uint32(model.FileRenameEventType) ev.RenameFile = model.RenameFileEvent{ Old: model.FimFileEvent{ - FileObject: uint64(arg.fileObject), - PathnameStr: path, - BasenameStr: filepath.Base(path), + FileObject: uint64(arg.fileObject), + PathnameStr: fileCache.fileName, + UserPathnameStr: fileCache.userFileName, + BasenameStr: filepath.Base(fileCache.fileName), }, New: model.FimFileEvent{ - FileObject: uint64(arg.fileObject), - PathnameStr: arg.filePath, - BasenameStr: filepath.Base(arg.filePath), + FileObject: uint64(arg.fileObject), + PathnameStr: arg.filePath, + UserPathnameStr: arg.userFilePath, + BasenameStr: filepath.Base(arg.filePath), }, } p.renamePreArgs.Remove(uint64(arg.fileObject)) @@ -625,18 +637,20 @@ func (p *WindowsProbe) handleETWNotification(ev *model.Event, notif etwNotificat ev.Type = uint32(model.DeleteFileEventType) ev.DeleteFile = model.DeleteFileEvent{ File: model.FimFileEvent{ - FileObject: uint64(arg.fileObject), - PathnameStr: arg.fileName, - BasenameStr: filepath.Base(arg.fileName), + FileObject: uint64(arg.fileObject), + PathnameStr: arg.fileName, + UserPathnameStr: arg.userFileName, + BasenameStr: filepath.Base(arg.fileName), }, } case *writeArgs: ev.Type = uint32(model.WriteFileEventType) ev.WriteFile = model.WriteFileEvent{ File: model.FimFileEvent{ - FileObject: uint64(arg.fileObject), - PathnameStr: arg.fileName, - BasenameStr: filepath.Base(arg.fileName), + FileObject: uint64(arg.fileObject), + PathnameStr: arg.fileName, + UserPathnameStr: arg.userFileName, + BasenameStr: filepath.Base(arg.fileName), }, } @@ -870,7 +884,7 @@ func NewWindowsProbe(probe *Probe, config *config.Config, opts Opts) (*WindowsPr return nil, err } - rnc, err := lru.New[uint64, string](5) + rnc, err := lru.New[uint64, fileCache](5) if err != nil { return nil, err } diff --git a/pkg/security/secl/model/accessors_windows.go b/pkg/security/secl/model/accessors_windows.go index 8e1f99823532f4..85e28ce00f8476 100644 --- a/pkg/security/secl/model/accessors_windows.go +++ b/pkg/security/secl/model/accessors_windows.go @@ -103,6 +103,26 @@ func (m *Model) GetEvaluator(field eval.Field, regID eval.RegisterID) (eval.Eval Field: field, Weight: eval.HandlerWeight, }, nil + case "create.file.path": + return &eval.StringEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.CreateNewFile.File) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil + case "create.file.path.length": + return &eval.IntEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) int { + ev := ctx.Event.(*Event) + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.CreateNewFile.File)) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil case "create.registry.key_name": return &eval.StringEvaluator{ EvalFnc: func(ctx *eval.Context) string { @@ -219,6 +239,26 @@ func (m *Model) GetEvaluator(field eval.Field, regID eval.RegisterID) (eval.Eval Field: field, Weight: eval.HandlerWeight, }, nil + case "delete.file.path": + return &eval.StringEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.DeleteFile.File) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil + case "delete.file.path.length": + return &eval.IntEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) int { + ev := ctx.Event.(*Event) + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.DeleteFile.File)) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil case "delete.registry.key_name": return &eval.StringEvaluator{ EvalFnc: func(ctx *eval.Context) string { @@ -1260,6 +1300,26 @@ func (m *Model) GetEvaluator(field eval.Field, regID eval.RegisterID) (eval.Eval Field: field, Weight: eval.HandlerWeight, }, nil + case "rename.file.destination.path": + return &eval.StringEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.New) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil + case "rename.file.destination.path.length": + return &eval.IntEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) int { + ev := ctx.Event.(*Event) + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.New)) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil case "rename.file.device_path": return &eval.StringEvaluator{ OpOverrides: eval.WindowsPathCmp, @@ -1300,6 +1360,26 @@ func (m *Model) GetEvaluator(field eval.Field, regID eval.RegisterID) (eval.Eval Field: field, Weight: eval.HandlerWeight, }, nil + case "rename.file.path": + return &eval.StringEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.Old) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil + case "rename.file.path.length": + return &eval.IntEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) int { + ev := ctx.Event.(*Event) + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.Old)) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil case "set.registry.key_name": return &eval.StringEvaluator{ EvalFnc: func(ctx *eval.Context) string { @@ -1470,6 +1550,26 @@ func (m *Model) GetEvaluator(field eval.Field, regID eval.RegisterID) (eval.Eval Field: field, Weight: eval.HandlerWeight, }, nil + case "write.file.path": + return &eval.StringEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) string { + ev := ctx.Event.(*Event) + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.WriteFile.File) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil + case "write.file.path.length": + return &eval.IntEvaluator{ + OpOverrides: eval.WindowsPathCmp, + EvalFnc: func(ctx *eval.Context) int { + ev := ctx.Event.(*Event) + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.WriteFile.File)) + }, + Field: field, + Weight: eval.HandlerWeight, + }, nil } return nil, &eval.ErrFieldNotFound{Field: field} } @@ -1482,6 +1582,8 @@ func (ev *Event) GetFields() []eval.Field { "create.file.device_path.length", "create.file.name", "create.file.name.length", + "create.file.path", + "create.file.path.length", "create.registry.key_name", "create.registry.key_name.length", "create.registry.key_path", @@ -1494,6 +1596,8 @@ func (ev *Event) GetFields() []eval.Field { "delete.file.device_path.length", "delete.file.name", "delete.file.name.length", + "delete.file.path", + "delete.file.path.length", "delete.registry.key_name", "delete.registry.key_name.length", "delete.registry.key_path", @@ -1585,10 +1689,14 @@ func (ev *Event) GetFields() []eval.Field { "rename.file.destination.device_path.length", "rename.file.destination.name", "rename.file.destination.name.length", + "rename.file.destination.path", + "rename.file.destination.path.length", "rename.file.device_path", "rename.file.device_path.length", "rename.file.name", "rename.file.name.length", + "rename.file.path", + "rename.file.path.length", "set.registry.key_name", "set.registry.key_name.length", "set.registry.key_path", @@ -1607,6 +1715,8 @@ func (ev *Event) GetFields() []eval.Field { "write.file.device_path.length", "write.file.name", "write.file.name.length", + "write.file.path", + "write.file.path.length", } } func (ev *Event) GetFieldValue(field eval.Field) (interface{}, error) { @@ -1625,6 +1735,10 @@ func (ev *Event) GetFieldValue(field eval.Field) (interface{}, error) { return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.CreateNewFile.File), nil case "create.file.name.length": return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.CreateNewFile.File), nil + case "create.file.path": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.CreateNewFile.File), nil + case "create.file.path.length": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.CreateNewFile.File), nil case "create.registry.key_name": return ev.CreateRegistryKey.Registry.KeyName, nil case "create.registry.key_name.length": @@ -1649,6 +1763,10 @@ func (ev *Event) GetFieldValue(field eval.Field) (interface{}, error) { return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.DeleteFile.File), nil case "delete.file.name.length": return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.DeleteFile.File), nil + case "delete.file.path": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.DeleteFile.File), nil + case "delete.file.path.length": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.DeleteFile.File), nil case "delete.registry.key_name": return ev.DeleteRegistryKey.Registry.KeyName, nil case "delete.registry.key_name.length": @@ -1994,6 +2112,10 @@ func (ev *Event) GetFieldValue(field eval.Field) (interface{}, error) { return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.RenameFile.New), nil case "rename.file.destination.name.length": return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.RenameFile.New), nil + case "rename.file.destination.path": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.New), nil + case "rename.file.destination.path.length": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.New), nil case "rename.file.device_path": return ev.FieldHandlers.ResolveFimFilePath(ev, &ev.RenameFile.Old), nil case "rename.file.device_path.length": @@ -2002,6 +2124,10 @@ func (ev *Event) GetFieldValue(field eval.Field) (interface{}, error) { return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.RenameFile.Old), nil case "rename.file.name.length": return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.RenameFile.Old), nil + case "rename.file.path": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.Old), nil + case "rename.file.path.length": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.Old), nil case "set.registry.key_name": return ev.SetRegistryKeyValue.Registry.KeyName, nil case "set.registry.key_name.length": @@ -2038,6 +2164,10 @@ func (ev *Event) GetFieldValue(field eval.Field) (interface{}, error) { return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.WriteFile.File), nil case "write.file.name.length": return ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.WriteFile.File), nil + case "write.file.path": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.WriteFile.File), nil + case "write.file.path.length": + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.WriteFile.File), nil } return nil, &eval.ErrFieldNotFound{Field: field} } @@ -2057,6 +2187,10 @@ func (ev *Event) GetFieldEventType(field eval.Field) (eval.EventType, error) { return "create", nil case "create.file.name.length": return "create", nil + case "create.file.path": + return "create", nil + case "create.file.path.length": + return "create", nil case "create.registry.key_name": return "create_key", nil case "create.registry.key_name.length": @@ -2081,6 +2215,10 @@ func (ev *Event) GetFieldEventType(field eval.Field) (eval.EventType, error) { return "delete", nil case "delete.file.name.length": return "delete", nil + case "delete.file.path": + return "delete", nil + case "delete.file.path.length": + return "delete", nil case "delete.registry.key_name": return "delete_key", nil case "delete.registry.key_name.length": @@ -2263,6 +2401,10 @@ func (ev *Event) GetFieldEventType(field eval.Field) (eval.EventType, error) { return "rename", nil case "rename.file.destination.name.length": return "rename", nil + case "rename.file.destination.path": + return "rename", nil + case "rename.file.destination.path.length": + return "rename", nil case "rename.file.device_path": return "rename", nil case "rename.file.device_path.length": @@ -2271,6 +2413,10 @@ func (ev *Event) GetFieldEventType(field eval.Field) (eval.EventType, error) { return "rename", nil case "rename.file.name.length": return "rename", nil + case "rename.file.path": + return "rename", nil + case "rename.file.path.length": + return "rename", nil case "set.registry.key_name": return "set_key_value", nil case "set.registry.key_name.length": @@ -2307,6 +2453,10 @@ func (ev *Event) GetFieldEventType(field eval.Field) (eval.EventType, error) { return "write", nil case "write.file.name.length": return "write", nil + case "write.file.path": + return "write", nil + case "write.file.path.length": + return "write", nil } return "", &eval.ErrFieldNotFound{Field: field} } @@ -2326,6 +2476,10 @@ func (ev *Event) GetFieldType(field eval.Field) (reflect.Kind, error) { return reflect.String, nil case "create.file.name.length": return reflect.Int, nil + case "create.file.path": + return reflect.String, nil + case "create.file.path.length": + return reflect.Int, nil case "create.registry.key_name": return reflect.String, nil case "create.registry.key_name.length": @@ -2350,6 +2504,10 @@ func (ev *Event) GetFieldType(field eval.Field) (reflect.Kind, error) { return reflect.String, nil case "delete.file.name.length": return reflect.Int, nil + case "delete.file.path": + return reflect.String, nil + case "delete.file.path.length": + return reflect.Int, nil case "delete.registry.key_name": return reflect.String, nil case "delete.registry.key_name.length": @@ -2532,6 +2690,10 @@ func (ev *Event) GetFieldType(field eval.Field) (reflect.Kind, error) { return reflect.String, nil case "rename.file.destination.name.length": return reflect.Int, nil + case "rename.file.destination.path": + return reflect.String, nil + case "rename.file.destination.path.length": + return reflect.Int, nil case "rename.file.device_path": return reflect.String, nil case "rename.file.device_path.length": @@ -2540,6 +2702,10 @@ func (ev *Event) GetFieldType(field eval.Field) (reflect.Kind, error) { return reflect.String, nil case "rename.file.name.length": return reflect.Int, nil + case "rename.file.path": + return reflect.String, nil + case "rename.file.path.length": + return reflect.Int, nil case "set.registry.key_name": return reflect.String, nil case "set.registry.key_name.length": @@ -2576,6 +2742,10 @@ func (ev *Event) GetFieldType(field eval.Field) (reflect.Kind, error) { return reflect.String, nil case "write.file.name.length": return reflect.Int, nil + case "write.file.path": + return reflect.String, nil + case "write.file.path.length": + return reflect.Int, nil } return reflect.Invalid, &eval.ErrFieldNotFound{Field: field} } @@ -2632,6 +2802,15 @@ func (ev *Event) SetFieldValue(field eval.Field, value interface{}) error { return nil case "create.file.name.length": return &eval.ErrFieldReadOnly{Field: "create.file.name.length"} + case "create.file.path": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "CreateNewFile.File.UserPathnameStr"} + } + ev.CreateNewFile.File.UserPathnameStr = rv + return nil + case "create.file.path.length": + return &eval.ErrFieldReadOnly{Field: "create.file.path.length"} case "create.registry.key_name": rv, ok := value.(string) if !ok { @@ -2686,6 +2865,15 @@ func (ev *Event) SetFieldValue(field eval.Field, value interface{}) error { return nil case "delete.file.name.length": return &eval.ErrFieldReadOnly{Field: "delete.file.name.length"} + case "delete.file.path": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "DeleteFile.File.UserPathnameStr"} + } + ev.DeleteFile.File.UserPathnameStr = rv + return nil + case "delete.file.path.length": + return &eval.ErrFieldReadOnly{Field: "delete.file.path.length"} case "delete.registry.key_name": rv, ok := value.(string) if !ok { @@ -3526,6 +3714,15 @@ func (ev *Event) SetFieldValue(field eval.Field, value interface{}) error { return nil case "rename.file.destination.name.length": return &eval.ErrFieldReadOnly{Field: "rename.file.destination.name.length"} + case "rename.file.destination.path": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "RenameFile.New.UserPathnameStr"} + } + ev.RenameFile.New.UserPathnameStr = rv + return nil + case "rename.file.destination.path.length": + return &eval.ErrFieldReadOnly{Field: "rename.file.destination.path.length"} case "rename.file.device_path": rv, ok := value.(string) if !ok { @@ -3544,6 +3741,15 @@ func (ev *Event) SetFieldValue(field eval.Field, value interface{}) error { return nil case "rename.file.name.length": return &eval.ErrFieldReadOnly{Field: "rename.file.name.length"} + case "rename.file.path": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "RenameFile.Old.UserPathnameStr"} + } + ev.RenameFile.Old.UserPathnameStr = rv + return nil + case "rename.file.path.length": + return &eval.ErrFieldReadOnly{Field: "rename.file.path.length"} case "set.registry.key_name": rv, ok := value.(string) if !ok { @@ -3630,6 +3836,15 @@ func (ev *Event) SetFieldValue(field eval.Field, value interface{}) error { return nil case "write.file.name.length": return &eval.ErrFieldReadOnly{Field: "write.file.name.length"} + case "write.file.path": + rv, ok := value.(string) + if !ok { + return &eval.ErrValueTypeMismatch{Field: "WriteFile.File.UserPathnameStr"} + } + ev.WriteFile.File.UserPathnameStr = rv + return nil + case "write.file.path.length": + return &eval.ErrFieldReadOnly{Field: "write.file.path.length"} } return &eval.ErrFieldNotFound{Field: field} } diff --git a/pkg/security/secl/model/field_accessors_windows.go b/pkg/security/secl/model/field_accessors_windows.go index ddddacb0fc6ed9..bcc19759044f33 100644 --- a/pkg/security/secl/model/field_accessors_windows.go +++ b/pkg/security/secl/model/field_accessors_windows.go @@ -69,6 +69,22 @@ func (ev *Event) GetCreateFileNameLength() int { return len(ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.CreateNewFile.File)) } +// GetCreateFilePath returns the value of the field, resolving if necessary +func (ev *Event) GetCreateFilePath() string { + if ev.GetEventType().String() != "create" { + return "" + } + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.CreateNewFile.File) +} + +// GetCreateFilePathLength returns the value of the field, resolving if necessary +func (ev *Event) GetCreateFilePathLength() int { + if ev.GetEventType().String() != "create" { + return 0 + } + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.CreateNewFile.File)) +} + // GetCreateRegistryKeyName returns the value of the field, resolving if necessary func (ev *Event) GetCreateRegistryKeyName() string { if ev.GetEventType().String() != "create_key" { @@ -165,6 +181,22 @@ func (ev *Event) GetDeleteFileNameLength() int { return len(ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.DeleteFile.File)) } +// GetDeleteFilePath returns the value of the field, resolving if necessary +func (ev *Event) GetDeleteFilePath() string { + if ev.GetEventType().String() != "delete" { + return "" + } + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.DeleteFile.File) +} + +// GetDeleteFilePathLength returns the value of the field, resolving if necessary +func (ev *Event) GetDeleteFilePathLength() int { + if ev.GetEventType().String() != "delete" { + return 0 + } + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.DeleteFile.File)) +} + // GetDeleteRegistryKeyName returns the value of the field, resolving if necessary func (ev *Event) GetDeleteRegistryKeyName() string { if ev.GetEventType().String() != "delete_key" { @@ -1325,6 +1357,22 @@ func (ev *Event) GetRenameFileDestinationNameLength() int { return len(ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.RenameFile.New)) } +// GetRenameFileDestinationPath returns the value of the field, resolving if necessary +func (ev *Event) GetRenameFileDestinationPath() string { + if ev.GetEventType().String() != "rename" { + return "" + } + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.New) +} + +// GetRenameFileDestinationPathLength returns the value of the field, resolving if necessary +func (ev *Event) GetRenameFileDestinationPathLength() int { + if ev.GetEventType().String() != "rename" { + return 0 + } + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.New)) +} + // GetRenameFileDevicePath returns the value of the field, resolving if necessary func (ev *Event) GetRenameFileDevicePath() string { if ev.GetEventType().String() != "rename" { @@ -1357,6 +1405,22 @@ func (ev *Event) GetRenameFileNameLength() int { return len(ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.RenameFile.Old)) } +// GetRenameFilePath returns the value of the field, resolving if necessary +func (ev *Event) GetRenameFilePath() string { + if ev.GetEventType().String() != "rename" { + return "" + } + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.Old) +} + +// GetRenameFilePathLength returns the value of the field, resolving if necessary +func (ev *Event) GetRenameFilePathLength() int { + if ev.GetEventType().String() != "rename" { + return 0 + } + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.Old)) +} + // GetSetRegistryKeyName returns the value of the field, resolving if necessary func (ev *Event) GetSetRegistryKeyName() string { if ev.GetEventType().String() != "set_key_value" { @@ -1505,3 +1569,19 @@ func (ev *Event) GetWriteFileNameLength() int { } return len(ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.WriteFile.File)) } + +// GetWriteFilePath returns the value of the field, resolving if necessary +func (ev *Event) GetWriteFilePath() string { + if ev.GetEventType().String() != "write" { + return "" + } + return ev.FieldHandlers.ResolveFileUserPath(ev, &ev.WriteFile.File) +} + +// GetWriteFilePathLength returns the value of the field, resolving if necessary +func (ev *Event) GetWriteFilePathLength() int { + if ev.GetEventType().String() != "write" { + return 0 + } + return len(ev.FieldHandlers.ResolveFileUserPath(ev, &ev.WriteFile.File)) +} diff --git a/pkg/security/secl/model/field_handlers_windows.go b/pkg/security/secl/model/field_handlers_windows.go index d0c1c0f90c45fb..314d81f64b8f8f 100644 --- a/pkg/security/secl/model/field_handlers_windows.go +++ b/pkg/security/secl/model/field_handlers_windows.go @@ -62,10 +62,12 @@ func (ev *Event) resolveFields(forADs bool) { switch ev.GetEventType().String() { case "create": _ = ev.FieldHandlers.ResolveFimFilePath(ev, &ev.CreateNewFile.File) + _ = ev.FieldHandlers.ResolveFileUserPath(ev, &ev.CreateNewFile.File) _ = ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.CreateNewFile.File) case "create_key": case "delete": _ = ev.FieldHandlers.ResolveFimFilePath(ev, &ev.DeleteFile.File) + _ = ev.FieldHandlers.ResolveFileUserPath(ev, &ev.DeleteFile.File) _ = ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.DeleteFile.File) case "delete_key": case "exec": @@ -87,12 +89,15 @@ func (ev *Event) resolveFields(forADs bool) { case "open_key": case "rename": _ = ev.FieldHandlers.ResolveFimFilePath(ev, &ev.RenameFile.Old) + _ = ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.Old) _ = ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.RenameFile.Old) _ = ev.FieldHandlers.ResolveFimFilePath(ev, &ev.RenameFile.New) + _ = ev.FieldHandlers.ResolveFileUserPath(ev, &ev.RenameFile.New) _ = ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.RenameFile.New) case "set_key_value": case "write": _ = ev.FieldHandlers.ResolveFimFilePath(ev, &ev.WriteFile.File) + _ = ev.FieldHandlers.ResolveFileUserPath(ev, &ev.WriteFile.File) _ = ev.FieldHandlers.ResolveFimFileBasename(ev, &ev.WriteFile.File) } } @@ -105,6 +110,7 @@ type FieldHandlers interface { ResolveEventTimestamp(ev *Event, e *BaseEvent) int ResolveFileBasename(ev *Event, e *FileEvent) string ResolveFilePath(ev *Event, e *FileEvent) string + ResolveFileUserPath(ev *Event, e *FimFileEvent) string ResolveFimFileBasename(ev *Event, e *FimFileEvent) string ResolveFimFilePath(ev *Event, e *FimFileEvent) string ResolveProcessCmdLine(ev *Event, e *Process) string @@ -134,6 +140,9 @@ func (dfh *FakeFieldHandlers) ResolveFileBasename(ev *Event, e *FileEvent) strin return e.BasenameStr } func (dfh *FakeFieldHandlers) ResolveFilePath(ev *Event, e *FileEvent) string { return e.PathnameStr } +func (dfh *FakeFieldHandlers) ResolveFileUserPath(ev *Event, e *FimFileEvent) string { + return e.UserPathnameStr +} func (dfh *FakeFieldHandlers) ResolveFimFileBasename(ev *Event, e *FimFileEvent) string { return e.BasenameStr } diff --git a/pkg/security/secl/model/model_windows.go b/pkg/security/secl/model/model_windows.go index 8cd636dbbae0a8..c5187007741442 100644 --- a/pkg/security/secl/model/model_windows.go +++ b/pkg/security/secl/model/model_windows.go @@ -54,9 +54,10 @@ type FileEvent struct { // FimFileEvent is the common file event type type FimFileEvent struct { - FileObject uint64 `field:"-"` // handle numeric value - PathnameStr string `field:"device_path,handler:ResolveFimFilePath,opts:length" op_override:"eval.WindowsPathCmp"` // SECLDoc[device_path] Definition:`File's path` Example:`create.file.device_path == "\device\harddisk1\cmd.bat"` Description:`Matches the creation of the file located at c:\cmd.bat` - BasenameStr string `field:"name,handler:ResolveFimFileBasename,opts:length" op_override:"eval.CaseInsensitiveCmp"` // SECLDoc[name] Definition:`File's basename` Example:`create.file.name == "cmd.bat"` Description:`Matches the creation of any file named cmd.bat.` + FileObject uint64 `field:"-"` // handle numeric value + PathnameStr string `field:"device_path,handler:ResolveFimFilePath,opts:length" op_override:"eval.WindowsPathCmp"` // SECLDoc[device_path] Definition:`File's path` Example:`create.file.device_path == "\device\harddisk1\cmd.bat"` Description:`Matches the creation of the file located at c:\cmd.bat` + UserPathnameStr string `field:"path,handler:ResolveFileUserPath,opts:length" op_override:"eval.WindowsPathCmp"` // SECLDoc[path] Definition:`File's path` Example:`create.file.path == "c:\cmd.bat"` Description:`Matches the creation of the file located at c:\cmd.bat` + BasenameStr string `field:"name,handler:ResolveFimFileBasename,opts:length" op_override:"eval.CaseInsensitiveCmp"` // SECLDoc[name] Definition:`File's basename` Example:`create.file.name == "cmd.bat"` Description:`Matches the creation of any file named cmd.bat.` } // RegistryEvent is the common registry event type diff --git a/pkg/security/tests/file_windows_test.go b/pkg/security/tests/file_windows_test.go index d9895ec7f4965e..8e559f8d6f475f 100644 --- a/pkg/security/tests/file_windows_test.go +++ b/pkg/security/tests/file_windows_test.go @@ -25,7 +25,7 @@ func TestBasicFileTest(t *testing.T) { //ebpftest.LogLevel(t, "info") cfn := &rules.RuleDefinition{ ID: "test_create_file", - Expression: `create.file.name =~ "test.bad" && create.file.device_path =~ "\Device\*\Temp\**"`, + Expression: `create.file.name =~ "test.bad" && create.file.path =~ "C:\Temp\**"`, } opts := testOpts{ enableFIM: true, @@ -68,7 +68,7 @@ func TestRenameFileEvent(t *testing.T) { // ebpftest.LogLevel(t, "info") cfn := &rules.RuleDefinition{ ID: "test_rename_file", - Expression: `rename.file.name =~ "test.bad" && rename.file.device_path =~ "\Device\*\Temp\**"`, + Expression: `rename.file.name =~ "test.bad" && rename.file.path =~ "C:\Temp\**"`, } opts := testOpts{ enableFIM: true, @@ -105,7 +105,7 @@ func TestDeleteFileEvent(t *testing.T) { // ebpftest.LogLevel(t, "info") cfn := &rules.RuleDefinition{ ID: "test_delete_file", - Expression: `delete.file.name =~ "test.bad" && delete.file.device_path =~ "\Device\*\Temp\**"`, + Expression: `delete.file.name =~ "test.bad" && delete.file.path =~ "C:\Temp\**"`, } opts := testOpts{ enableFIM: true, @@ -141,7 +141,7 @@ func TestWriteFileEvent(t *testing.T) { // ebpftest.LogLevel(t, "info") cfn := &rules.RuleDefinition{ ID: "test_write_file", - Expression: `write.file.name =~ "test.bad" && write.file.device_path =~ "\Device\*\Temp\**"`, + Expression: `write.file.name =~ "test.bad" && write.file.path =~ "C:\Temp\**"`, } opts := testOpts{ enableFIM: true, From e5b0c6697ab5b2c4a71df2c701ddb265f9bcbb62 Mon Sep 17 00:00:00 2001 From: Jon Bodner Date: Fri, 24 May 2024 08:24:33 -0400 Subject: [PATCH 18/32] Properly calculate the start time for a service and use the right value for LastHeartbeat (#25840) * The start time for a process is in clicks (100 per second) since machine boot. Get the boot time and do the calculation properly. Use the LastHeartbeat in the event telemetry, don't generate a new value. * add test to validate timer and realTime * protect against an empty command line * add copyright header --- .../corechecks/servicediscovery/events.go | 4 +- .../servicediscovery/events_test.go | 2 - .../corechecks/servicediscovery/impl_linux.go | 18 +++- .../servicediscovery/impl_linux_test.go | 87 ++++++++++--------- .../servicediscovery/language/language.go | 3 + .../servicediscovery/service_detector_test.go | 19 ++++ .../servicediscovery/servicediscovery_test.go | 21 +++++ 7 files changed, 106 insertions(+), 48 deletions(-) create mode 100644 pkg/collector/corechecks/servicediscovery/servicediscovery_test.go diff --git a/pkg/collector/corechecks/servicediscovery/events.go b/pkg/collector/corechecks/servicediscovery/events.go index 209543a26781de..c51ac877fb4ee2 100644 --- a/pkg/collector/corechecks/servicediscovery/events.go +++ b/pkg/collector/corechecks/servicediscovery/events.go @@ -46,7 +46,6 @@ type event struct { type telemetrySender struct { sender sender.Sender - time timer hostname hostname.Component } @@ -70,7 +69,7 @@ func (ts *telemetrySender) newEvent(t eventType, svc serviceInfo) *event { ServiceLanguage: svc.meta.Language, ServiceType: svc.meta.Type, StartTime: int64(svc.process.Stat.StartTime), - LastSeen: ts.time.Now().Unix(), + LastSeen: svc.LastHeartbeat.Unix(), APMInstrumentation: svc.meta.APMInstrumentation, ServiceNameSource: nameSource, }, @@ -80,7 +79,6 @@ func (ts *telemetrySender) newEvent(t eventType, svc serviceInfo) *event { func newTelemetrySender(sender sender.Sender) *telemetrySender { return &telemetrySender{ sender: sender, - time: realTime{}, hostname: hostnameimpl.NewHostnameService(), } } diff --git a/pkg/collector/corechecks/servicediscovery/events_test.go b/pkg/collector/corechecks/servicediscovery/events_test.go index 7ff4c2c90a2e93..2060d5eff2d15d 100644 --- a/pkg/collector/corechecks/servicediscovery/events_test.go +++ b/pkg/collector/corechecks/servicediscovery/events_test.go @@ -52,7 +52,6 @@ func Test_telemetrySender(t *testing.T) { ts := newTelemetrySender(mSender) ts.hostname = mHostname - ts.time = mTimer svc := serviceInfo{ process: processInfo{ @@ -152,7 +151,6 @@ func Test_telemetrySender_name_provided(t *testing.T) { ts := newTelemetrySender(mSender) ts.hostname = mHostname - ts.time = mTimer svc := serviceInfo{ process: processInfo{ diff --git a/pkg/collector/corechecks/servicediscovery/impl_linux.go b/pkg/collector/corechecks/servicediscovery/impl_linux.go index 13d9ae552114ef..181295e28eacab 100644 --- a/pkg/collector/corechecks/servicediscovery/impl_linux.go +++ b/pkg/collector/corechecks/servicediscovery/impl_linux.go @@ -38,6 +38,7 @@ type linuxImpl struct { procfs procFS portPoller portPoller time timer + bootTime uint64 serviceDetector *serviceDetector sender *telemetrySender @@ -60,8 +61,13 @@ func newLinuxImpl(sender *telemetrySender, ignoreCfg map[string]bool) (osImpl, e if err != nil { return nil, err } + stat, err := pfs.Stat() + if err != nil { + return nil, err + } return &linuxImpl{ procfs: wProcFS{pfs}, + bootTime: stat.BootTime, portPoller: poller, time: realTime{}, sender: sender, @@ -122,10 +128,13 @@ func (li *linuxImpl) DiscoverServices() error { stopped []serviceInfo ) + now := li.time.Now() + // potentialServices contains processes that we scanned in the previous iteration and had open ports. // we check if they are still alive in this iteration, and if so, we send a start-service telemetry event. for pid, svc := range li.potentialServices { if _, ok := procs[pid]; ok { + svc.LastHeartbeat = now li.aliveServices[pid] = svc started = append(started, *svc) } @@ -163,14 +172,13 @@ func (li *linuxImpl) DiscoverServices() error { } // check if services previously marked as alive still are. - now := li.time.Now() for pid, svc := range li.aliveServices { if _, ok := procs[pid]; !ok { delete(li.aliveServices, pid) stopped = append(stopped, *svc) } else if now.Sub(svc.LastHeartbeat).Truncate(time.Minute) >= heartbeatTime { - li.sender.sendHeartbeatServiceEvent(*svc) svc.LastHeartbeat = now + li.sender.sendHeartbeatServiceEvent(*svc) } } @@ -270,13 +278,17 @@ func (li *linuxImpl) getServiceInfo(p proc, openPorts map[int]portlist.List) (*s // for now, docker-proxy is going on the ignore list + // calculate the start time + // divide Starttime by 100 to go from clicks since boot to seconds since boot + startTimeSecs := li.bootTime + (stat.Starttime / 100) + pInfo := processInfo{ PID: p.PID(), CmdLine: cmdline, Env: env, Cwd: cwd, Stat: procStat{ - StartTime: stat.Starttime, + StartTime: startTimeSecs, }, Ports: ports, } diff --git a/pkg/collector/corechecks/servicediscovery/impl_linux_test.go b/pkg/collector/corechecks/servicediscovery/impl_linux_test.go index e994fc67e64b44..19912a70dbfe35 100644 --- a/pkg/collector/corechecks/servicediscovery/impl_linux_test.go +++ b/pkg/collector/corechecks/servicediscovery/impl_linux_test.go @@ -33,8 +33,10 @@ type testProc struct { } var ( - startTime = time.Date(2024, 5, 13, 0, 0, 0, 0, time.UTC) - procLaunchedTime = startTime.Add(-1 * time.Hour) + bootTimeSeconds = uint64(time.Date(2000, 01, 01, 0, 0, 0, 0, time.UTC).Unix()) + // procLaunched is number of clicks (100 per second) since bootTime when the process started + // assume it's 12 hours later + procLaunchedClicks = uint64((12 * time.Hour).Seconds()) * 100 ) var ( @@ -44,7 +46,7 @@ var ( env: nil, cwd: "", stat: procfs.ProcStat{ - Starttime: uint64(procLaunchedTime.Unix()), + Starttime: procLaunchedClicks, }, } procTestService1 = testProc{ @@ -53,7 +55,7 @@ var ( env: []string{}, cwd: "", stat: procfs.ProcStat{ - Starttime: uint64(procLaunchedTime.Unix()), + Starttime: procLaunchedClicks, }, } procIgnoreService1 = testProc{ @@ -62,7 +64,7 @@ var ( env: nil, cwd: "", stat: procfs.ProcStat{ - Starttime: uint64(procLaunchedTime.Unix()), + Starttime: procLaunchedClicks, }, } procTestService1Repeat = testProc{ @@ -71,7 +73,7 @@ var ( env: []string{}, cwd: "", stat: procfs.ProcStat{ - Starttime: uint64(procLaunchedTime.Unix()), + Starttime: procLaunchedClicks, }, } procTestService1DifferentPID = testProc{ @@ -80,7 +82,7 @@ var ( env: []string{}, cwd: "", stat: procfs.ProcStat{ - Starttime: uint64(procLaunchedTime.Unix()), + Starttime: procLaunchedClicks, }, } ) @@ -131,6 +133,11 @@ func mockProc( return m } +func calcTime(additionalTime time.Duration) time.Time { + unix := time.Unix(int64(bootTimeSeconds+(procLaunchedClicks/100)), 0) + return unix.Add(additionalTime) +} + // cmpEvents is used to sort event slices in tests. // It returns true if a is smaller than b, false otherwise. func cmpEvents(a, b *event) bool { @@ -186,7 +193,7 @@ func Test_linuxImpl(t *testing.T) { portTCP8080, portTCP8081, }, - time: startTime, + time: calcTime(0), }, { aliveProcs: []testProc{ @@ -199,7 +206,7 @@ func Test_linuxImpl(t *testing.T) { portTCP8080, portTCP8081, }, - time: startTime.Add(1 * time.Minute), + time: calcTime(1 * time.Minute), }, { aliveProcs: []testProc{ @@ -212,7 +219,7 @@ func Test_linuxImpl(t *testing.T) { portTCP8080, portTCP8081, }, - time: startTime.Add(20 * time.Minute), + time: calcTime(20 * time.Minute), }, { aliveProcs: []testProc{ @@ -221,7 +228,7 @@ func Test_linuxImpl(t *testing.T) { openPorts: []portlist.Port{ portTCP22, }, - time: startTime.Add(21 * time.Minute), + time: calcTime(21 * time.Minute), }, }, wantEvents: []*event{ @@ -235,8 +242,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "web_service", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(1 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(1 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -251,8 +258,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "web_service", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(20 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(20 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -267,8 +274,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "web_service", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(21 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(20 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -293,7 +300,7 @@ func Test_linuxImpl(t *testing.T) { portTCP8081, portTCP5432, }, - time: startTime, + time: calcTime(0), }, { aliveProcs: []testProc{ @@ -308,7 +315,7 @@ func Test_linuxImpl(t *testing.T) { portTCP8081, portTCP5432, }, - time: startTime.Add(1 * time.Minute), + time: calcTime(1 * time.Minute), }, { aliveProcs: []testProc{ @@ -323,7 +330,7 @@ func Test_linuxImpl(t *testing.T) { portTCP8081, portTCP5432, }, - time: startTime.Add(20 * time.Minute), + time: calcTime(20 * time.Minute), }, { aliveProcs: []testProc{ @@ -334,7 +341,7 @@ func Test_linuxImpl(t *testing.T) { portTCP22, portTCP8080, }, - time: startTime.Add(21 * time.Minute), + time: calcTime(21 * time.Minute), }, }, wantEvents: []*event{ @@ -348,8 +355,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "web_service", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(1 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(1 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -364,8 +371,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "db", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(1 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(1 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -380,8 +387,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "web_service", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(20 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(20 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -396,8 +403,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "db", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(20 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(20 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -412,8 +419,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "db", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(21 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(20 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -436,7 +443,7 @@ func Test_linuxImpl(t *testing.T) { portTCP8080, portTCP8081, }, - time: startTime, + time: calcTime(0), }, { aliveProcs: []testProc{ @@ -449,7 +456,7 @@ func Test_linuxImpl(t *testing.T) { portTCP8080, portTCP8081, }, - time: startTime.Add(1 * time.Minute), + time: calcTime(1 * time.Minute), }, { aliveProcs: []testProc{ @@ -460,7 +467,7 @@ func Test_linuxImpl(t *testing.T) { portTCP22, portTCP8080DifferentPID, }, - time: startTime.Add(21 * time.Minute), + time: calcTime(21 * time.Minute), }, { aliveProcs: []testProc{ @@ -471,7 +478,7 @@ func Test_linuxImpl(t *testing.T) { portTCP22, portTCP8080DifferentPID, }, - time: startTime.Add(22 * time.Minute), + time: calcTime(22 * time.Minute), }, }, wantEvents: []*event{ @@ -485,8 +492,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "web_service", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(1 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(1 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -501,8 +508,8 @@ func Test_linuxImpl(t *testing.T) { Env: "", ServiceLanguage: "UNKNOWN", ServiceType: "web_service", - StartTime: procLaunchedTime.Unix(), - LastSeen: startTime.Add(22 * time.Minute).Unix(), + StartTime: calcTime(0).Unix(), + LastSeen: calcTime(22 * time.Minute).Unix(), APMInstrumentation: "none", ServiceNameSource: "generated", }, @@ -553,8 +560,8 @@ func Test_linuxImpl(t *testing.T) { check.os.(*linuxImpl).procfs = mProcFS check.os.(*linuxImpl).portPoller = mPortPoller check.os.(*linuxImpl).time = mTimer - check.os.(*linuxImpl).sender.time = mTimer check.os.(*linuxImpl).sender.hostname = mHostname + check.os.(*linuxImpl).bootTime = bootTimeSeconds err = check.Run() require.NoError(t, err) diff --git a/pkg/collector/corechecks/servicediscovery/language/language.go b/pkg/collector/corechecks/servicediscovery/language/language.go index 91500a878ab8ed..7aaee3ba4eac2d 100644 --- a/pkg/collector/corechecks/servicediscovery/language/language.go +++ b/pkg/collector/corechecks/servicediscovery/language/language.go @@ -81,6 +81,9 @@ type ProcessInfo struct { // FileReader attempts to read the most representative file associated to a process. func (pi ProcessInfo) FileReader() (io.ReadCloser, bool) { + if len(pi.Args) == 0 { + return nil, false + } fileName := pi.Args[0] // if it's an absolute path, use it if strings.HasPrefix(fileName, "/") { diff --git a/pkg/collector/corechecks/servicediscovery/service_detector_test.go b/pkg/collector/corechecks/servicediscovery/service_detector_test.go index 89a01bdb195c7a..5abd079e57d6e4 100644 --- a/pkg/collector/corechecks/servicediscovery/service_detector_test.go +++ b/pkg/collector/corechecks/servicediscovery/service_detector_test.go @@ -32,4 +32,23 @@ func Test_serviceDetector(t *testing.T) { } got := sd.Detect(pInfo) assert.Equal(t, want, got) + + // pass in nil slices and see if anything blows up + pInfoEmpty := processInfo{ + PID: 0, + CmdLine: nil, + Env: nil, + Cwd: "", + Stat: procStat{}, + Ports: nil, + } + wantEmpty := serviceMetadata{ + Name: "", + Language: "UNKNOWN", + Type: "web_service", + APMInstrumentation: "none", + FromDDService: false, + } + gotEmpty := sd.Detect(pInfoEmpty) + assert.Equal(t, wantEmpty, gotEmpty) } diff --git a/pkg/collector/corechecks/servicediscovery/servicediscovery_test.go b/pkg/collector/corechecks/servicediscovery/servicediscovery_test.go new file mode 100644 index 00000000000000..17eb38dbc5c648 --- /dev/null +++ b/pkg/collector/corechecks/servicediscovery/servicediscovery_test.go @@ -0,0 +1,21 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +package servicediscovery + +import ( + "testing" + "time" +) + +func TestTimer(t *testing.T) { + var myTimer timer = realTime{} + val := myTimer.Now() + compare := time.Now() + // should be basically the same time, within a millisecond + if compare.Sub(val).Truncate(time.Millisecond) != 0 { + t.Errorf("expected within a millisecond: %v, %v", compare, val) + } +} From b20d40e7104c08c5a43ed7430309329b22f69380 Mon Sep 17 00:00:00 2001 From: Jonathan Ribas Date: Fri, 24 May 2024 14:34:11 +0200 Subject: [PATCH 19/32] Detail setup error when user is not root (#25883) --- cmd/cws-instrumentation/subcommands/setupcmd/setup.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmd/cws-instrumentation/subcommands/setupcmd/setup.go b/cmd/cws-instrumentation/subcommands/setupcmd/setup.go index cc45c85182a597..d8d25bf7933c41 100644 --- a/cmd/cws-instrumentation/subcommands/setupcmd/setup.go +++ b/cmd/cws-instrumentation/subcommands/setupcmd/setup.go @@ -68,7 +68,10 @@ func setupCWSInjector(params *setupCliParams) error { targetPath := filepath.Join(params.cwsVolumeMount, filepath.Base(path)) target, err := os.Create(targetPath) if err != nil { - return fmt.Errorf("couldn't create target cws-instrumentation binary file in the mounted volume") + if os.IsPermission(err) && os.Getuid() != 0 { + return fmt.Errorf("couldn't copy cws-instrumentation binary file in the mounted volume: %v. Current UID: %d, you may want to use user 0 instead", err, os.Getuid()) + } + return fmt.Errorf("couldn't copy cws-instrumentation binary file in the mounted volume: %v", err) } defer target.Close() From 520aa18bd54ef0e27315bef1d77fa74e16fbba25 Mon Sep 17 00:00:00 2001 From: Marethyu <45374460+Pythyu@users.noreply.github.com> Date: Fri, 24 May 2024 14:45:19 +0200 Subject: [PATCH 20/32] =?UTF-8?q?[2024=20GPG=20KEY=20ROTATION]=C2=A0Rotate?= =?UTF-8?q?=20GPG=20key=20used=20to=20build=20the=20agent=20(#25821)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(GPG): rotate GPG keys used to build the agent * feat(GPG): remove v2 in key name * feat(release): add release notes * Update 2024-gpg-key-rotation-0aceffce7a9771fa.yaml * Update releasenotes/notes/2024-gpg-key-rotation-0aceffce7a9771fa.yaml Co-authored-by: DeForest Richards <56796055+drichards-87@users.noreply.github.com> --------- Co-authored-by: DeForest Richards <56796055+drichards-87@users.noreply.github.com> --- .gitlab-ci.yml | 14 +++++++------- .../2024-gpg-key-rotation-0aceffce7a9771fa.yaml | 13 +++++++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/2024-gpg-key-rotation-0aceffce7a9771fa.yaml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d82ad7c02dd1f7..63597ecc8246fa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -175,9 +175,9 @@ variables: DATADOG_AGENT_BTF_GEN_BUILDIMAGES: v34815905-ae40295e DATADOG_AGENT_EMBEDDED_PATH: /opt/datadog-agent/embedded - DEB_GPG_KEY_ID: ad9589b7 - DEB_GPG_KEY_NAME: "Datadog, Inc. Master key" - RPM_GPG_KEY_ID: fd4bf915 + DEB_GPG_KEY_ID: c0962c7d + DEB_GPG_KEY_NAME: "Datadog, Inc. APT key" + RPM_GPG_KEY_ID: b01082d3 RPM_GPG_KEY_NAME: "Datadog, Inc. RPM key" DOCKER_REGISTRY_URL: docker.io KITCHEN_INFRASTRUCTURE_FLAKES_RETRY: 2 @@ -205,8 +205,8 @@ variables: CHANGELOG_COMMIT_SHA_SSM_NAME: ci.datadog-agent.gitlab_changelog_commit_sha # agent-ci-experience CHOCOLATEY_API_KEY_SSM_NAME: ci.datadog-agent.chocolatey_api_key # windows-agent CODECOV_TOKEN_SSM_NAME: ci.datadog-agent.codecov_token # agent-ci-experience - DEB_GPG_KEY_SSM_NAME: ci.datadog-agent.deb_signing_private_key_v2_${DEB_GPG_KEY_ID} # agent-build-and-releases - DEB_SIGNING_PASSPHRASE_SSM_NAME: ci.datadog-agent.deb_signing_key_passphrase_v2_${DEB_GPG_KEY_ID} # agent-build-and-releases + DEB_GPG_KEY_SSM_NAME: ci.datadog-agent.deb_signing_private_key_${DEB_GPG_KEY_ID} # agent-build-and-releases + DEB_SIGNING_PASSPHRASE_SSM_NAME: ci.datadog-agent.deb_signing_key_passphrase_${DEB_GPG_KEY_ID} # agent-build-and-releases DOCKER_REGISTRY_LOGIN_SSM_KEY: ci.datadog-agent.docker_hub_login # agent-ci-experience DOCKER_REGISTRY_PWD_SSM_KEY: ci.datadog-agent.docker_hub_pwd # agent-ci-experience E2E_TESTS_API_KEY_SSM_NAME: ci.datadog-agent.e2e_tests_api_key # agent-developer-tools @@ -227,8 +227,8 @@ variables: MACOS_GITHUB_APP_ID_2_SSM_NAME: ci.datadog-agent.macos_github_app_id_2 # agent-ci-experience MACOS_GITHUB_INSTALLATION_ID_2_SSM_NAME: ci.datadog-agent.macos_github_installation_id_2 # agent-ci-experience MACOS_GITHUB_KEY_2_SSM_NAME: ci.datadog-agent.macos_github_key_b64_2 # agent-ci-experience - RPM_GPG_KEY_SSM_NAME: ci.datadog-agent.rpm_signing_private_key_v2_${RPM_GPG_KEY_ID} # agent-build-and-releases - RPM_SIGNING_PASSPHRASE_SSM_NAME: ci.datadog-agent.rpm_signing_key_passphrase_v2_${RPM_GPG_KEY_ID} # agent-build-and-releases + RPM_GPG_KEY_SSM_NAME: ci.datadog-agent.rpm_signing_private_key_${RPM_GPG_KEY_ID} # agent-build-and-releases + RPM_SIGNING_PASSPHRASE_SSM_NAME: ci.datadog-agent.rpm_signing_key_passphrase_${RPM_GPG_KEY_ID} # agent-build-and-releases SMP_ACCOUNT_ID_SSM_NAME: ci.datadog-agent.single-machine-performance-account-id # single-machine-performance SMP_AGENT_TEAM_ID_SSM_NAME: ci.datadog-agent.single-machine-performance-agent-team-id # single-machine-performance SMP_API_SSM_NAME: ci.datadog-agent.single-machine-performance-api # single-machine-performance diff --git a/releasenotes/notes/2024-gpg-key-rotation-0aceffce7a9771fa.yaml b/releasenotes/notes/2024-gpg-key-rotation-0aceffce7a9771fa.yaml new file mode 100644 index 00000000000000..bc869c3960fe54 --- /dev/null +++ b/releasenotes/notes/2024-gpg-key-rotation-0aceffce7a9771fa.yaml @@ -0,0 +1,13 @@ +# Each section from every release note are combined when the +# CHANGELOG.rst is rendered. So the text needs to be worded so that +# it does not depend on any information only available in another +# section. This may mean repeating some details, but each section +# must be readable independently of the other. +# +# Each section note must be formatted as reStructuredText. +--- +security: + - | + Current GPG keys that are used to sign new releases of the Agent package are about to expire. + Following our 2024 GPG key rotation plan, we rotated RPM and APT GPG keys. + From c2052df07c1e72e31521b3e4803ff13d025e51da Mon Sep 17 00:00:00 2001 From: Alex Lopez Date: Fri, 24 May 2024 15:07:49 +0200 Subject: [PATCH 21/32] Add ruby gem caching to more omnibus-based jobs (#25888) --- .gitlab/package_build/deb.yml | 6 ++++++ .gitlab/package_build/installer.yml | 3 +++ .gitlab/package_build/rpm.yml | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/.gitlab/package_build/deb.yml b/.gitlab/package_build/deb.yml index b9521264f22189..f617bc05142d49 100644 --- a/.gitlab/package_build/deb.yml +++ b/.gitlab/package_build/deb.yml @@ -142,6 +142,7 @@ agent_deb-arm64-a7: - !reference [.setup_ruby_mirror_linux] - !reference [.setup_python_mirror_linux] - !reference [.retrieve_linux_go_deps] + - !reference [.cache_omnibus_ruby_deps, setup] - echo "About to build for $RELEASE_VERSION_7" - echo "Detected host architecture $(uname -m)" # $DD_TARGET_ARCH is only set by Arm build images, so assume amd64 if not present @@ -165,6 +166,8 @@ agent_deb-arm64-a7: expire_in: 2 weeks paths: - $OMNIBUS_PACKAGE_DIR + cache: + - !reference [.cache_omnibus_ruby_deps, cache] iot_agent_deb-x64: extends: .iot_agent_build_common_deb @@ -224,6 +227,7 @@ dogstatsd_deb-x64: before_script: - source /root/.bashrc - !reference [.retrieve_linux_go_deps] + - !reference [.cache_omnibus_ruby_deps, setup] script: # remove artifacts from previous pipelines that may come from the cache - rm -rf $OMNIBUS_PACKAGE_DIR/* @@ -241,6 +245,8 @@ dogstatsd_deb-x64: expire_in: 2 weeks paths: - $OMNIBUS_PACKAGE_DIR + cache: + - !reference [.cache_omnibus_ruby_deps, cache] dogstatsd_deb-arm64: rules: diff --git a/.gitlab/package_build/installer.yml b/.gitlab/package_build/installer.yml index 0bedec04c142de..5eca919ef705fe 100644 --- a/.gitlab/package_build/installer.yml +++ b/.gitlab/package_build/installer.yml @@ -92,6 +92,7 @@ datadog-agent-oci-arm64-a7: - source /root/.bashrc - !reference [.setup_ruby_mirror_linux] - !reference [.retrieve_linux_go_deps] + - !reference [.cache_omnibus_ruby_deps, setup] - echo "About to build for $RELEASE_VERSION" # remove artifacts from previous pipelines that may come from the cache - rm -rf $OMNIBUS_PACKAGE_DIR/* @@ -107,6 +108,8 @@ datadog-agent-oci-arm64-a7: expire_in: 2 weeks paths: - $OMNIBUS_PACKAGE_DIR + cache: + - !reference [.cache_omnibus_ruby_deps, cache] # We build a "regular" installer, meant to be packaged as deb/rpm, without a custom install path # and an artifact intended for OCI packaging, which has a custom install path diff --git a/.gitlab/package_build/rpm.yml b/.gitlab/package_build/rpm.yml index 65a1d709666872..de6f0af92d6467 100644 --- a/.gitlab/package_build/rpm.yml +++ b/.gitlab/package_build/rpm.yml @@ -144,6 +144,7 @@ agent_rpm-arm64-a7: - !reference [.setup_ruby_mirror_linux] - !reference [.setup_python_mirror_linux] - !reference [.retrieve_linux_go_deps] + - !reference [.cache_omnibus_ruby_deps, setup] - echo "Detected host architecture $(uname -m)" # $DD_TARGET_ARCH is only set by Arm build images, so assume amd64 if not present - echo "Target architecture ${DD_TARGET_ARCH:=amd64}" @@ -167,6 +168,8 @@ agent_rpm-arm64-a7: expire_in: 2 weeks paths: - $OMNIBUS_PACKAGE_DIR + cache: + - !reference [.cache_omnibus_ruby_deps, cache] iot_agent_rpm-x64: extends: .iot_agent_build_common_rpm @@ -223,6 +226,7 @@ dogstatsd_rpm-x64: before_script: - source /root/.bashrc - !reference [.retrieve_linux_go_deps] + - !reference [.cache_omnibus_ruby_deps, setup] script: # remove artifacts from previous pipelines that may come from the cache - rm -rf $OMNIBUS_PACKAGE_DIR/* @@ -242,3 +246,5 @@ dogstatsd_rpm-x64: expire_in: 2 weeks paths: - $OMNIBUS_PACKAGE_DIR + cache: + - !reference [.cache_omnibus_ruby_deps, cache] From d230ccd03a461d417fcb0cca20ea370e658dd2c5 Mon Sep 17 00:00:00 2001 From: Kevin Fairise <132568982+KevinFairise2@users.noreply.github.com> Date: Fri, 24 May 2024 15:07:59 +0200 Subject: [PATCH 22/32] ADXT-200 Fail early if the fakeintake restarted during a test (#25345) * Fail early if the fakeintake ID restarted during a test * Add mutex around fakeintakeID + disable StrictCheckMode until all tests use new server [skip cancel] * init mutex [skip cancel] * Enable strict check mode by default * Create const for fakeintake id header * Enable strict check mode to see if e2e pre tests work * Disable strict check for now [skip cancel] * Go mod tidy * Test with strict mode to check e2e pre_tests * Disable strict mode [skip cancel] * Do not check if no fakeintake ID header * Update test/fakeintake/server/server_test.go Co-authored-by: Florent Clarret * Update test/fakeintake/client/client.go Co-authored-by: pducolin <45568537+pducolin@users.noreply.github.com> * Remove shutdown changes * Client now panic on fakeintake restart/timeout --------- Co-authored-by: Florent Clarret Co-authored-by: pducolin <45568537+pducolin@users.noreply.github.com> --- test/fakeintake/client/client.go | 54 +++++++++- test/fakeintake/client/client_test.go | 120 ++++++++++++++++------ test/fakeintake/cmd/server/main.go | 2 +- test/fakeintake/go.mod | 2 +- test/fakeintake/server/server.go | 13 ++- test/fakeintake/server/server_test.go | 15 ++- test/new-e2e/pkg/components/fakeintake.go | 2 +- 7 files changed, 165 insertions(+), 43 deletions(-) diff --git a/test/fakeintake/client/client.go b/test/fakeintake/client/client.go index c73b6581f4efd3..d80a6f3e0a1a6b 100644 --- a/test/fakeintake/client/client.go +++ b/test/fakeintake/client/client.go @@ -44,9 +44,11 @@ import ( "errors" "fmt" "io" + "net" "net/http" "regexp" "strings" + "sync" "time" "github.com/cenkalti/backoff/v4" @@ -59,6 +61,7 @@ import ( ) const ( + fakeintakeIDHeader = "Fakeintake-ID" metricsEndpoint = "/api/v2/series" checkRunsEndpoint = "/api/v1/check_run" logsEndpoint = "/api/v2/logs" @@ -81,9 +84,22 @@ const ( // ErrNoFlareAvailable is returned when no flare is available var ErrNoFlareAvailable = errors.New("no flare available") +// Option is a configuration option for the client +type Option func(*Client) + +// WithoutStrictFakeintakeIDCheck disables strict fakeintake ID check +func WithoutStrictFakeintakeIDCheck() Option { + return func(c *Client) { + c.strictFakeintakeIDCheck = false + } +} + // Client is a fake intake client type Client struct { - fakeIntakeURL string + fakeintakeID string + fakeIntakeURL string + strictFakeintakeIDCheck bool + fakeintakeIDMutex sync.RWMutex metricAggregator aggregator.MetricAggregator checkRunAggregator aggregator.CheckRunAggregator @@ -105,8 +121,10 @@ type Client struct { // NewClient creates a new fake intake client // fakeIntakeURL: the host of the fake Datadog intake server -func NewClient(fakeIntakeURL string) *Client { - return &Client{ +func NewClient(fakeIntakeURL string, opts ...Option) *Client { + client := &Client{ + strictFakeintakeIDCheck: true, + fakeintakeIDMutex: sync.RWMutex{}, fakeIntakeURL: strings.TrimSuffix(fakeIntakeURL, "/"), metricAggregator: aggregator.NewMetricAggregator(), checkRunAggregator: aggregator.NewCheckRunAggregator(), @@ -125,6 +143,11 @@ func NewClient(fakeIntakeURL string) *Client { metadataAggregator: aggregator.NewMetadataAggregator(), ndmflowAggregator: aggregator.NewNDMFlowAggregator(), } + for _, opt := range opts { + opt(client) + } + + return client } // PayloadFilter is used to filter payloads by name and resource type @@ -782,13 +805,36 @@ func (c *Client) get(route string) ([]byte, error) { var body []byte err := backoff.Retry(func() error { tmpResp, err := http.Get(fmt.Sprintf("%s/%s", c.fakeIntakeURL, route)) + if err, ok := err.(net.Error); ok && err.Timeout() { + panic("fakeintake call timed out") + } if err != nil { return err } + defer tmpResp.Body.Close() if tmpResp.StatusCode != http.StatusOK { - return fmt.Errorf("Expected %d got %d", http.StatusOK, tmpResp.StatusCode) + return fmt.Errorf("expected %d got %d", http.StatusOK, tmpResp.StatusCode) } + // If strictFakeintakeIDCheck is enabled, we check that the fakeintake ID is the same as the one we expect + // If the fakeintake ID is not set yet we set the one we get from the first request + // If the fakeintake does not return its id in the header we do not check it + requestFakeintakeID := tmpResp.Header.Get(fakeintakeIDHeader) + if c.strictFakeintakeIDCheck && requestFakeintakeID != "" { + if c.fakeintakeID == "" { + c.fakeintakeIDMutex.Lock() + c.fakeintakeID = requestFakeintakeID + c.fakeintakeIDMutex.Unlock() + } else { + c.fakeintakeIDMutex.RLock() + currentFakeintakeID := c.fakeintakeID + c.fakeintakeIDMutex.RUnlock() + if currentFakeintakeID != requestFakeintakeID { + panic(fmt.Sprintf("expected fakeintakeID %s got %s: The fakeintake probably restarted during your test", currentFakeintakeID, requestFakeintakeID)) + } + } + } + body, err = io.ReadAll(tmpResp.Body) return err }, backoff.WithMaxRetries(backoff.NewConstantBackOff(5*time.Second), 4)) diff --git a/test/fakeintake/client/client_test.go b/test/fakeintake/client/client_test.go index e45564b4541ebb..0567b5347fa9e3 100644 --- a/test/fakeintake/client/client_test.go +++ b/test/fakeintake/client/client_test.go @@ -56,9 +56,18 @@ var apiV1Metadata []byte //go:embed fixtures/api_v2_ndmflow_response var apiV2NDMFlow []byte +func NewServer(handler http.Handler) *httptest.Server { + handlerWitHeader := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Fakeintake-ID", "20000000-0000-0000-0000-000000000000") + handler.ServeHTTP(w, r) + }) + + return httptest.NewServer(handlerWitHeader) +} + func TestClient(t *testing.T) { t.Run("getFakePayloads should properly format the request", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // allow requests only to "/foo/bar" routes := r.URL.Query()["endpoint"] @@ -92,7 +101,7 @@ func TestClient(t *testing.T) { }) t.Run("getFakePayloads should handle response with errors", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) })) defer ts.Close() @@ -104,7 +113,7 @@ func TestClient(t *testing.T) { }) t.Run("getMetrics", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2SeriesResponse) })) defer ts.Close() @@ -119,7 +128,7 @@ func TestClient(t *testing.T) { }) t.Run("getMetric", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2SeriesResponse) })) defer ts.Close() @@ -132,7 +141,7 @@ func TestClient(t *testing.T) { }) t.Run("FilterMetrics", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2SeriesResponse) })) defer ts.Close() @@ -148,7 +157,7 @@ func TestClient(t *testing.T) { }) t.Run("getCheckRun", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV1CheckRunResponse) })) defer ts.Close() @@ -163,7 +172,7 @@ func TestClient(t *testing.T) { }) t.Run("GetCheckRun", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV1CheckRunResponse) })) defer ts.Close() @@ -176,7 +185,7 @@ func TestClient(t *testing.T) { }) t.Run("getLogs", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2LogsResponse) })) defer ts.Close() @@ -189,7 +198,7 @@ func TestClient(t *testing.T) { }) t.Run("getLog", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2LogsResponse) })) defer ts.Close() @@ -204,7 +213,7 @@ func TestClient(t *testing.T) { }) t.Run("FilterLogs", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2LogsResponse) })) defer ts.Close() @@ -216,7 +225,7 @@ func TestClient(t *testing.T) { }) t.Run("GetServerHealth", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/fakeintake/health" { w.WriteHeader(http.StatusBadRequest) return @@ -231,7 +240,7 @@ func TestClient(t *testing.T) { }) t.Run("FlushPayloads", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/fakeintake/flushPayloads" { w.WriteHeader(http.StatusBadRequest) return @@ -246,7 +255,7 @@ func TestClient(t *testing.T) { }) t.Run("ConfigureOverride", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/fakeintake/configure/override" { w.WriteHeader(http.StatusBadRequest) return @@ -265,7 +274,7 @@ func TestClient(t *testing.T) { }) t.Run("GetLatestFlare", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(supportFlareResponse) })) defer ts.Close() @@ -291,7 +300,7 @@ func TestClient(t *testing.T) { ] }`, base64.StdEncoding.EncodeToString(payload)) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(response)) })) defer ts.Close() @@ -316,7 +325,7 @@ func TestClient(t *testing.T) { ] }`, base64.StdEncoding.EncodeToString(payload)) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(response)) })) defer ts.Close() @@ -341,7 +350,7 @@ func TestClient(t *testing.T) { ] }`, base64.StdEncoding.EncodeToString(payload)) - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(response)) })) defer ts.Close() @@ -354,7 +363,7 @@ func TestClient(t *testing.T) { }) t.Run("getContainerImages", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2ContainerImage) })) defer ts.Close() @@ -369,7 +378,7 @@ func TestClient(t *testing.T) { }) t.Run("getContainerImage", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2ContainerImage) })) defer ts.Close() @@ -382,7 +391,7 @@ func TestClient(t *testing.T) { }) t.Run("FilterContainerImages", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2ContainerImage) })) defer ts.Close() @@ -395,7 +404,7 @@ func TestClient(t *testing.T) { }) t.Run("getContainerLifecycleEvents", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2ContainerLifecycle) })) defer ts.Close() @@ -409,7 +418,7 @@ func TestClient(t *testing.T) { }) t.Run("GetContainerLifecycleEvents", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2ContainerLifecycle) })) defer ts.Close() @@ -421,7 +430,7 @@ func TestClient(t *testing.T) { }) t.Run("getSBOMs", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2SBOM) })) defer ts.Close() @@ -436,7 +445,7 @@ func TestClient(t *testing.T) { }) t.Run("getSBOM", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2SBOM) })) defer ts.Close() @@ -449,7 +458,7 @@ func TestClient(t *testing.T) { }) t.Run("FilterSBOMs", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2SBOM) })) defer ts.Close() @@ -462,7 +471,7 @@ func TestClient(t *testing.T) { }) t.Run("getTraces", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV02Trace) })) defer ts.Close() @@ -474,7 +483,7 @@ func TestClient(t *testing.T) { }) t.Run("getAPMStats", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV02APMStats) })) defer ts.Close() @@ -486,7 +495,7 @@ func TestClient(t *testing.T) { }) t.Run("GetMetadata", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV1Metadata) })) defer ts.Close() @@ -502,7 +511,7 @@ func TestClient(t *testing.T) { }) t.Run("getNDMFlows", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2NDMFlow) })) defer ts.Close() @@ -514,7 +523,7 @@ func TestClient(t *testing.T) { }) t.Run("GetNDMFlows", func(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write(apiV2NDMFlow) })) defer ts.Close() @@ -556,4 +565,55 @@ func TestClient(t *testing.T) { assert.Equal(t, "172.199.15.1", ndmflows[0].NextHop.IP) assert.Empty(t, ndmflows[0].AdditionalFields) }) + + t.Run("test strict fakeintakeid check mode", func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + assert.Equal(t, "expected fakeintakeID 20000000-0000-0000-0000-000000000000 got 10000000-0000-0000-0000-000000000000: The fakeintake probably restarted during your test", r) + } + }() + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/fakeintake/health" { + w.Header().Set("Fakeintake-ID", "10000000-0000-0000-0000-000000000000") + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Fakeintake-ID", "20000000-0000-0000-0000-000000000000") + + })) + defer ts.Close() + + client := NewClient(ts.URL) + + _, err := client.get("fakeintake/toto") + require.NoError(t, err) + _, err = client.get("fakeintake/hello") + require.NoError(t, err) + client.get("fakeintake/health") + // should never be called because we expect previous call to panic + t.Fail() + }) + + t.Run("test non-strict fakeintakeid check mode", func(t *testing.T) { + ts := NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/fakeintake/health" { + w.Header().Set("Fakeintake-ID", "10000000-0000-0000-0000-000000000000") + w.WriteHeader(http.StatusOK) + return + } + w.Header().Set("Fakeintake-ID", "20000000-0000-0000-0000-000000000000") + + })) + defer ts.Close() + + client := NewClient(ts.URL, WithoutStrictFakeintakeIDCheck()) + + _, err := client.get("fakeintake/toto") + require.NoError(t, err) + _, err = client.get("fakeintake/hello") + require.NoError(t, err) + _, err = client.get("fakeintake/health") + require.NoError(t, err) + }) + } diff --git a/test/fakeintake/cmd/server/main.go b/test/fakeintake/cmd/server/main.go index 1d53558cda4d42..c686443e4016ac 100644 --- a/test/fakeintake/cmd/server/main.go +++ b/test/fakeintake/cmd/server/main.go @@ -47,7 +47,6 @@ func main() { timeout.Stop() log.Printf("🏃 Fake intake running at %s", fi.URL()) - <-sigs log.Println("Stopping fake intake") err := fi.Stop() @@ -57,4 +56,5 @@ func main() { log.Println("Fake intake is stopped") log.Println("👋 Bye bye") + } diff --git a/test/fakeintake/go.mod b/test/fakeintake/go.mod index 2b513c063a6c2f..24e41c1526198d 100644 --- a/test/fakeintake/go.mod +++ b/test/fakeintake/go.mod @@ -14,6 +14,7 @@ require ( github.com/DataDog/datadog-agent/pkg/proto v0.54.0-rc.2 github.com/benbjohnson/clock v1.3.5 github.com/cenkalti/backoff/v4 v4.2.1 + github.com/google/uuid v1.3.0 github.com/kr/pretty v0.3.1 github.com/olekukonko/tablewriter v0.0.5 github.com/prometheus/client_golang v1.17.0 @@ -35,7 +36,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/test/fakeintake/server/server.go b/test/fakeintake/server/server.go index 3edb40deea25bd..bde4bdc9a3035d 100644 --- a/test/fakeintake/server/server.go +++ b/test/fakeintake/server/server.go @@ -28,6 +28,7 @@ import ( "time" "github.com/benbjohnson/clock" + "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/collectors" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -59,6 +60,7 @@ type Option func(*Server) // Server is a struct implementing a fakeintake server type Server struct { + uuid uuid.UUID server http.Server ready chan bool clock clock.Clock @@ -111,6 +113,7 @@ func NewServer(options ...Option) *Server { ) mux := http.NewServeMux() + mux.HandleFunc("/", fi.handleDatadogRequest) mux.HandleFunc("/fakeintake/payloads", fi.handleGetPayloads) mux.HandleFunc("/fakeintake/health", fi.handleFakeHealth) @@ -130,7 +133,12 @@ func NewServer(options ...Option) *Server { Registry: registry, })) - fi.server.Handler = mux + fi.server.Handler = func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Fakeintake-ID", fi.uuid.String()) + next.ServeHTTP(w, r) + }) + }(mux) return fi } @@ -209,6 +217,7 @@ func (fi *Server) Start() { return } fi.shutdown = make(chan struct{}) + go fi.listenRoutine() go fi.cleanUpPayloadsRoutine() } @@ -242,8 +251,6 @@ func (fi *Server) Stop() error { if err != nil { return err } - - fi.setURL("") return nil } diff --git a/test/fakeintake/server/server_test.go b/test/fakeintake/server/server_test.go index 520448423986c0..336f9a81c3fd80 100644 --- a/test/fakeintake/server/server_test.go +++ b/test/fakeintake/server/server_test.go @@ -20,11 +20,10 @@ import ( "testing" "time" + "github.com/DataDog/datadog-agent/test/fakeintake/api" "github.com/benbjohnson/clock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/DataDog/datadog-agent/test/fakeintake/api" ) func TestServer(t *testing.T) { @@ -416,7 +415,6 @@ func testServer(t *testing.T, opts ...Option) { json.NewDecoder(cleanedResponse.Body).Decode(&getCleanedResponse) return len(getCleanedResponse.Payloads) == 2 }, 5*time.Second, 100*time.Millisecond, "should contain 2 elements after cleanup of only older elements") - fi.Stop() }) } @@ -595,6 +593,17 @@ func testServer(t *testing.T, opts ...Option) { require.NoError(t, err, "Error reading response body") assert.Equal(t, []byte(`{"errors":[]}`), responseBody) }) + + t.Run("should contain a Fakeintake-ID header", func(t *testing.T) { + fi, _ := InitialiseForTests(t, opts...) + defer fi.Stop() + + response, err := http.Get(fi.URL() + "/fakeintake/health") + require.NoError(t, err, "Error on GET request") + defer response.Body.Close() + assert.NotEmpty(t, response.Header.Get("Fakeintake-ID"), "Fakeintake-ID header is empty") + assert.Equal(t, http.StatusOK, response.StatusCode, "unexpected code") + }) } type TestTextPayload struct { diff --git a/test/new-e2e/pkg/components/fakeintake.go b/test/new-e2e/pkg/components/fakeintake.go index 7f8a4ca55ee6a4..876e237f7258a8 100644 --- a/test/new-e2e/pkg/components/fakeintake.go +++ b/test/new-e2e/pkg/components/fakeintake.go @@ -23,7 +23,7 @@ var _ e2e.Initializable = &FakeIntake{} // Init is called by e2e test Suite after the component is provisioned. func (fi *FakeIntake) Init(e2e.Context) error { - fi.client = client.NewClient(fi.URL) + fi.client = client.NewClient(fi.URL, client.WithoutStrictFakeintakeIDCheck()) //TODO: enable strict check mode when the fakeintake server change is merged on main and the new server is used return nil } From 8e4391bc55921bbd0e69dafb8b5da1be2e1cc979 Mon Sep 17 00:00:00 2001 From: Jeremy Hanna Date: Fri, 24 May 2024 09:20:20 -0400 Subject: [PATCH 23/32] [ASCII-1434] Migrate /dogstats* and eventplatformreceiver endpoints to providers (#25367) Co-authored-by: GustavoCaso --- cmd/agent/subcommands/run/command.go | 2 + comp/README.md | 4 + .../demultiplexerimpl/demultiplexer_mock.go | 17 +- .../demultiplexerendpoint/def/component.go | 13 ++ .../demultiplexerendpoint/fx-mock/fx.go | 21 ++ .../aggregator/demultiplexerendpoint/fx/fx.go | 21 ++ .../demultiplexerendpoint/impl/endpoint.go | 92 +++++++++ .../demultiplexerendpoint/mock/mock.go | 36 ++++ comp/api/api/apiimpl/api.go | 127 +++++------- comp/api/api/apiimpl/api_test.go | 41 ++-- comp/api/api/apiimpl/internal/agent/agent.go | 189 +----------------- .../api/apiimpl/internal/agent/agent_test.go | 21 +- comp/api/api/apiimpl/server.go | 9 - comp/api/api/apiimpl/server_cmd.go | 10 - comp/api/api/utils/stream/stream.go | 94 +++++++++ comp/dogstatsd/server/server.go | 17 +- comp/dogstatsd/server/server_mock.go | 29 ++- comp/dogstatsd/server/stats_endpoint.go | 56 ++++++ .../eventplatformimpl/epforwarder.go | 2 +- .../eventplatformreceiver.go | 23 ++- .../eventplatformreceiverimpl/mock.go | 24 ++- 21 files changed, 520 insertions(+), 328 deletions(-) create mode 100644 comp/aggregator/demultiplexerendpoint/def/component.go create mode 100644 comp/aggregator/demultiplexerendpoint/fx-mock/fx.go create mode 100644 comp/aggregator/demultiplexerendpoint/fx/fx.go create mode 100644 comp/aggregator/demultiplexerendpoint/impl/endpoint.go create mode 100644 comp/aggregator/demultiplexerendpoint/mock/mock.go create mode 100644 comp/api/api/utils/stream/stream.go create mode 100644 comp/dogstatsd/server/stats_endpoint.go diff --git a/cmd/agent/subcommands/run/command.go b/cmd/agent/subcommands/run/command.go index 080527181950ab..eba899b5d8b4ff 100644 --- a/cmd/agent/subcommands/run/command.go +++ b/cmd/agent/subcommands/run/command.go @@ -41,6 +41,7 @@ import ( "github.com/DataDog/datadog-agent/comp/agent/jmxlogger/jmxloggerimpl" "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer/demultiplexerimpl" + demultiplexerendpointfx "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexerendpoint/fx" internalAPI "github.com/DataDog/datadog-agent/comp/api/api" "github.com/DataDog/datadog-agent/comp/api/api/apiimpl" authtokenimpl "github.com/DataDog/datadog-agent/comp/api/authtoken/createandfetchimpl" @@ -368,6 +369,7 @@ func getSharedFxOption() fx.Option { apiimpl.Module(), compressionimpl.Module(), demultiplexerimpl.Module(), + demultiplexerendpointfx.Module(), dogstatsd.Bundle(), fx.Provide(func(logsagent optional.Option[logsAgent.Component]) optional.Option[logsagentpipeline.Component] { if la, ok := logsagent.Get(); ok { diff --git a/comp/README.md b/comp/README.md index 00bbf063319471..a425a86aa8266a 100644 --- a/comp/README.md +++ b/comp/README.md @@ -40,6 +40,10 @@ Package aggregator implements the "aggregator" bundle, Package demultiplexer defines the aggregator demultiplexer +### [comp/aggregator/demultiplexerendpoint](https://pkg.go.dev/github.com/DataDog/datadog-agent/comp/aggregator/demultiplexerendpoint) + +Package demultiplexerendpoint component provides the /dogstatsd-contexts-dump API endpoint that can register via Fx value groups. + ### [comp/aggregator/diagnosesendermanager](https://pkg.go.dev/github.com/DataDog/datadog-agent/comp/aggregator/diagnosesendermanager) *Datadog Team*: agent-shared-components diff --git a/comp/aggregator/demultiplexer/demultiplexerimpl/demultiplexer_mock.go b/comp/aggregator/demultiplexer/demultiplexerimpl/demultiplexer_mock.go index 3ae5b07d8872a7..102df45b249783 100644 --- a/comp/aggregator/demultiplexer/demultiplexerimpl/demultiplexer_mock.go +++ b/comp/aggregator/demultiplexer/demultiplexerimpl/demultiplexer_mock.go @@ -56,7 +56,16 @@ type mockDependencies struct { Hostname hostname.Component } -func newMock(deps mockDependencies) (demultiplexerComp.Component, demultiplexerComp.Mock, sender.SenderManager) { +// MockProvides is the mock component output +type MockProvides struct { + fx.Out + + Comp demultiplexerComp.Component + Mock demultiplexerComp.Mock + Sender sender.SenderManager +} + +func newMock(deps mockDependencies) MockProvides { opts := aggregator.DefaultAgentDemultiplexerOptions() opts.DontStartForwarders = true @@ -68,5 +77,9 @@ func newMock(deps mockDependencies) (demultiplexerComp.Component, demultiplexerC } instance := &mock{AgentDemultiplexer: aggregator.InitAndStartAgentDemultiplexerForTest(aggDeps, opts, "")} - return instance, instance, instance + return MockProvides{ + Comp: instance, + Mock: instance, + Sender: instance, + } } diff --git a/comp/aggregator/demultiplexerendpoint/def/component.go b/comp/aggregator/demultiplexerendpoint/def/component.go new file mode 100644 index 00000000000000..7288ff23bbfb57 --- /dev/null +++ b/comp/aggregator/demultiplexerendpoint/def/component.go @@ -0,0 +1,13 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2023-present Datadog, Inc. + +// Package demultiplexerendpoint component provides the /dogstatsd-contexts-dump API endpoint that can register via Fx value groups. +package demultiplexerendpoint + +// team: agent-metrics-logs + +// Component is the component type. +type Component interface { +} diff --git a/comp/aggregator/demultiplexerendpoint/fx-mock/fx.go b/comp/aggregator/demultiplexerendpoint/fx-mock/fx.go new file mode 100644 index 00000000000000..7a615278b26991 --- /dev/null +++ b/comp/aggregator/demultiplexerendpoint/fx-mock/fx.go @@ -0,0 +1,21 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +// Package fx provides the fx module for the demultiplexerendpoint component +package fx + +import ( + demultiplexerendpointmock "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexerendpoint/mock" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" +) + +// MockModule defines the fx options for this mock component +func MockModule() fxutil.Module { + return fxutil.Component( + fxutil.ProvideComponentConstructor( + demultiplexerendpointmock.NewMock, + ), + ) +} diff --git a/comp/aggregator/demultiplexerendpoint/fx/fx.go b/comp/aggregator/demultiplexerendpoint/fx/fx.go new file mode 100644 index 00000000000000..ef890efaf230be --- /dev/null +++ b/comp/aggregator/demultiplexerendpoint/fx/fx.go @@ -0,0 +1,21 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2024-present Datadog, Inc. + +// Package fx provides the fx module for the demultiplexerendpoint component +package fx + +import ( + demultiplexerendpointimpl "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexerendpoint/impl" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" +) + +// Module defines the fx options for this component +func Module() fxutil.Module { + return fxutil.Component( + fxutil.ProvideComponentConstructor( + demultiplexerendpointimpl.NewComponent, + ), + ) +} diff --git a/comp/aggregator/demultiplexerendpoint/impl/endpoint.go b/comp/aggregator/demultiplexerendpoint/impl/endpoint.go new file mode 100644 index 00000000000000..45980a555e9c92 --- /dev/null +++ b/comp/aggregator/demultiplexerendpoint/impl/endpoint.go @@ -0,0 +1,92 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2023-present Datadog, Inc. + +// Package demultiplexerendpointimpl component provides the /dogstatsd-contexts-dump API endpoint that can register via Fx value groups. +package demultiplexerendpointimpl + +import ( + "bufio" + "encoding/json" + "net/http" + "os" + "path" + + "github.com/DataDog/zstd" + + demultiplexerComp "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" + "github.com/DataDog/datadog-agent/comp/api/api" + "github.com/DataDog/datadog-agent/comp/api/api/utils" + "github.com/DataDog/datadog-agent/comp/core/config" + "github.com/DataDog/datadog-agent/comp/core/log" +) + +// Requires defines the dependencies for the demultiplexerendpoint component +type Requires struct { + Log log.Component + Config config.Component + Demultiplexer demultiplexerComp.Component +} + +type demultiplexerEndpoint struct { + demux demultiplexerComp.Component + config config.Component + log log.Component +} + +// Provides defines the output of the demultiplexerendpoint component +type Provides struct { + Endpoint api.AgentEndpointProvider +} + +// NewComponent creates a new demultiplexerendpoint component +func NewComponent(reqs Requires) Provides { + endpoint := demultiplexerEndpoint{ + demux: reqs.Demultiplexer, + config: reqs.Config, + log: reqs.Log, + } + + return Provides{ + Endpoint: api.NewAgentEndpointProvider(endpoint.dumpDogstatsdContexts, "/dogstatsd-contexts-dump", "POST"), + } +} + +func (demuxendpoint demultiplexerEndpoint) dumpDogstatsdContexts(w http.ResponseWriter, _ *http.Request) { + path, err := demuxendpoint.writeDogstatsdContexts() + if err != nil { + utils.SetJSONError(w, demuxendpoint.log.Errorf("Failed to create dogstatsd contexts dump: %v", err), 500) + return + } + + resp, err := json.Marshal(path) + if err != nil { + utils.SetJSONError(w, demuxendpoint.log.Errorf("Failed to serialize response: %v", err), 500) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(resp) +} + +func (demuxendpoint demultiplexerEndpoint) writeDogstatsdContexts() (string, error) { + path := path.Join(demuxendpoint.config.GetString("run_path"), "dogstatsd_contexts.json.zstd") + + f, err := os.Create(path) + if err != nil { + return "", err + } + + c := zstd.NewWriter(f) + + w := bufio.NewWriter(c) + + for _, err := range []error{demuxendpoint.demux.DumpDogstatsdContexts(w), w.Flush(), c.Close(), f.Close()} { + if err != nil { + return "", err + } + } + + return path, nil +} diff --git a/comp/aggregator/demultiplexerendpoint/mock/mock.go b/comp/aggregator/demultiplexerendpoint/mock/mock.go new file mode 100644 index 00000000000000..f0561fac850135 --- /dev/null +++ b/comp/aggregator/demultiplexerendpoint/mock/mock.go @@ -0,0 +1,36 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2023-present Datadog, Inc. + +//go:build test + +// Package demultiplexerendpointmock provides a mock component for demultiplexerendpoint +package demultiplexerendpointmock + +import ( + "net/http" + + "github.com/DataDog/datadog-agent/comp/api/api" +) + +type mock struct { +} + +// ServeHTTP is a simple mocked http.Handler function +func (m *mock) handlerFunc(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("OK")) +} + +// Provides is the mock component output +type Provides struct { + Endpoint api.AgentEndpointProvider +} + +// NewMock creates a new mock component +func NewMock() Provides { + instance := &mock{} + return Provides{ + Endpoint: api.NewAgentEndpointProvider(instance.handlerFunc, "/dogstatsd-contexts-dump", "POST"), + } +} diff --git a/comp/api/api/apiimpl/api.go b/comp/api/api/apiimpl/api.go index bf45641f7a9157..4cd2ef704b59eb 100644 --- a/comp/api/api/apiimpl/api.go +++ b/comp/api/api/apiimpl/api.go @@ -11,7 +11,6 @@ import ( "go.uber.org/fx" - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" "github.com/DataDog/datadog-agent/comp/api/api" "github.com/DataDog/datadog-agent/comp/api/authtoken" "github.com/DataDog/datadog-agent/comp/collector/collector" @@ -24,13 +23,7 @@ import ( "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" - dogstatsddebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" - "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" - "github.com/DataDog/datadog-agent/comp/metadata/host" - "github.com/DataDog/datadog-agent/comp/metadata/inventoryagent" - "github.com/DataDog/datadog-agent/comp/metadata/inventorychecks" - "github.com/DataDog/datadog-agent/comp/metadata/inventoryhost" "github.com/DataDog/datadog-agent/comp/metadata/packagesigning" "github.com/DataDog/datadog-agent/comp/remote-config/rcservice" "github.com/DataDog/datadog-agent/comp/remote-config/rcservicemrf" @@ -46,86 +39,65 @@ func Module() fxutil.Module { } type apiServer struct { - dogstatsdServer dogstatsdServer.Component - capture replay.Component - pidMap pidmap.Component - serverDebug dogstatsddebug.Component - hostMetadata host.Component - invAgent inventoryagent.Component - demux demultiplexer.Component - invHost inventoryhost.Component - secretResolver secrets.Component - invChecks inventorychecks.Component - pkgSigning packagesigning.Component - statusComponent status.Component - eventPlatformReceiver eventplatformreceiver.Component - rcService optional.Option[rcservice.Component] - rcServiceMRF optional.Option[rcservicemrf.Component] - authToken authtoken.Component - taggerComp tagger.Component - autoConfig autodiscovery.Component - logsAgentComp optional.Option[logsAgent.Component] - wmeta workloadmeta.Component - collector optional.Option[collector.Component] - gui optional.Option[gui.Component] - endpointProviders []api.EndpointProvider + dogstatsdServer dogstatsdServer.Component + capture replay.Component + pidMap pidmap.Component + secretResolver secrets.Component + pkgSigning packagesigning.Component + statusComponent status.Component + rcService optional.Option[rcservice.Component] + rcServiceMRF optional.Option[rcservicemrf.Component] + authToken authtoken.Component + taggerComp tagger.Component + autoConfig autodiscovery.Component + logsAgentComp optional.Option[logsAgent.Component] + wmeta workloadmeta.Component + collector optional.Option[collector.Component] + gui optional.Option[gui.Component] + endpointProviders []api.EndpointProvider } type dependencies struct { fx.In - DogstatsdServer dogstatsdServer.Component - Capture replay.Component - PidMap pidmap.Component - ServerDebug dogstatsddebug.Component - HostMetadata host.Component - InvAgent inventoryagent.Component - Demux demultiplexer.Component - InvHost inventoryhost.Component - SecretResolver secrets.Component - InvChecks inventorychecks.Component - PkgSigning packagesigning.Component - StatusComponent status.Component - EventPlatformReceiver eventplatformreceiver.Component - RcService optional.Option[rcservice.Component] - RcServiceMRF optional.Option[rcservicemrf.Component] - AuthToken authtoken.Component - Tagger tagger.Component - AutoConfig autodiscovery.Component - LogsAgentComp optional.Option[logsAgent.Component] - WorkloadMeta workloadmeta.Component - Collector optional.Option[collector.Component] - Gui optional.Option[gui.Component] - EndpointProviders []api.EndpointProvider `group:"agent_endpoint"` + DogstatsdServer dogstatsdServer.Component + Capture replay.Component + PidMap pidmap.Component + SecretResolver secrets.Component + PkgSigning packagesigning.Component + StatusComponent status.Component + RcService optional.Option[rcservice.Component] + RcServiceMRF optional.Option[rcservicemrf.Component] + AuthToken authtoken.Component + Tagger tagger.Component + AutoConfig autodiscovery.Component + LogsAgentComp optional.Option[logsAgent.Component] + WorkloadMeta workloadmeta.Component + Collector optional.Option[collector.Component] + Gui optional.Option[gui.Component] + EndpointProviders []api.EndpointProvider `group:"agent_endpoint"` } var _ api.Component = (*apiServer)(nil) func newAPIServer(deps dependencies) api.Component { return &apiServer{ - dogstatsdServer: deps.DogstatsdServer, - capture: deps.Capture, - pidMap: deps.PidMap, - serverDebug: deps.ServerDebug, - hostMetadata: deps.HostMetadata, - invAgent: deps.InvAgent, - demux: deps.Demux, - invHost: deps.InvHost, - secretResolver: deps.SecretResolver, - invChecks: deps.InvChecks, - pkgSigning: deps.PkgSigning, - statusComponent: deps.StatusComponent, - eventPlatformReceiver: deps.EventPlatformReceiver, - rcService: deps.RcService, - rcServiceMRF: deps.RcServiceMRF, - authToken: deps.AuthToken, - taggerComp: deps.Tagger, - autoConfig: deps.AutoConfig, - logsAgentComp: deps.LogsAgentComp, - wmeta: deps.WorkloadMeta, - collector: deps.Collector, - gui: deps.Gui, - endpointProviders: deps.EndpointProviders, + dogstatsdServer: deps.DogstatsdServer, + capture: deps.Capture, + pidMap: deps.PidMap, + secretResolver: deps.SecretResolver, + pkgSigning: deps.PkgSigning, + statusComponent: deps.StatusComponent, + rcService: deps.RcService, + rcServiceMRF: deps.RcServiceMRF, + authToken: deps.AuthToken, + taggerComp: deps.Tagger, + autoConfig: deps.AutoConfig, + logsAgentComp: deps.LogsAgentComp, + wmeta: deps.WorkloadMeta, + collector: deps.Collector, + gui: deps.Gui, + endpointProviders: deps.EndpointProviders, } } @@ -138,16 +110,13 @@ func (server *apiServer) StartServer( server.dogstatsdServer, server.capture, server.pidMap, - server.serverDebug, server.wmeta, server.taggerComp, server.logsAgentComp, senderManager, - server.demux, server.secretResolver, server.statusComponent, server.collector, - server.eventPlatformReceiver, server.autoConfig, server.gui, server.endpointProviders, diff --git a/comp/api/api/apiimpl/api_test.go b/comp/api/api/apiimpl/api_test.go index cd2a2bcc0ff091..aec84e79ee978e 100644 --- a/comp/api/api/apiimpl/api_test.go +++ b/comp/api/api/apiimpl/api_test.go @@ -35,13 +35,9 @@ import ( "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver" "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" - "github.com/DataDog/datadog-agent/comp/metadata/host" "github.com/DataDog/datadog-agent/comp/metadata/host/hostimpl" - "github.com/DataDog/datadog-agent/comp/metadata/inventoryagent" "github.com/DataDog/datadog-agent/comp/metadata/inventoryagent/inventoryagentimpl" - "github.com/DataDog/datadog-agent/comp/metadata/inventorychecks" "github.com/DataDog/datadog-agent/comp/metadata/inventorychecks/inventorychecksimpl" - "github.com/DataDog/datadog-agent/comp/metadata/inventoryhost" "github.com/DataDog/datadog-agent/comp/metadata/inventoryhost/inventoryhostimpl" "github.com/DataDog/datadog-agent/comp/metadata/packagesigning" "github.com/DataDog/datadog-agent/comp/metadata/packagesigning/packagesigningimpl" @@ -68,12 +64,8 @@ type testdeps struct { DogstatsdServer dogstatsdServer.Component Capture replay.Component ServerDebug dogstatsddebug.Component - HostMetadata host.Component - InvAgent inventoryagent.Component Demux demultiplexer.Component - InvHost inventoryhost.Component SecretResolver secrets.Component - InvChecks inventorychecks.Component PkgSigning packagesigning.Component StatusComponent status.Mock EventPlatformReceiver eventplatformreceiver.Component @@ -129,26 +121,19 @@ func getComponentDependencies(t *testing.T) testdeps { func getTestAPIServer(deps testdeps) api.Component { apideps := dependencies{ - DogstatsdServer: deps.DogstatsdServer, - Capture: deps.Capture, - ServerDebug: deps.ServerDebug, - HostMetadata: deps.HostMetadata, - InvAgent: deps.InvAgent, - Demux: deps.Demux, - InvHost: deps.InvHost, - SecretResolver: deps.SecretResolver, - InvChecks: deps.InvChecks, - PkgSigning: deps.PkgSigning, - StatusComponent: deps.StatusComponent, - EventPlatformReceiver: deps.EventPlatformReceiver, - RcService: deps.RcService, - RcServiceMRF: deps.RcServiceMRF, - AuthToken: deps.AuthToken, - Tagger: deps.Tagger, - LogsAgentComp: deps.Logs, - WorkloadMeta: deps.WorkloadMeta, - Collector: deps.Collector, - EndpointProviders: deps.EndpointProviders, + DogstatsdServer: deps.DogstatsdServer, + Capture: deps.Capture, + SecretResolver: deps.SecretResolver, + PkgSigning: deps.PkgSigning, + StatusComponent: deps.StatusComponent, + RcService: deps.RcService, + RcServiceMRF: deps.RcServiceMRF, + AuthToken: deps.AuthToken, + Tagger: deps.Tagger, + LogsAgentComp: deps.Logs, + WorkloadMeta: deps.WorkloadMeta, + Collector: deps.Collector, + EndpointProviders: deps.EndpointProviders, } return newAPIServer(apideps) } diff --git a/comp/api/api/apiimpl/internal/agent/agent.go b/comp/api/api/apiimpl/internal/agent/agent.go index 1009645e739b30..e7eb9e42d792f8 100644 --- a/comp/api/api/apiimpl/internal/agent/agent.go +++ b/comp/api/api/apiimpl/internal/agent/agent.go @@ -9,26 +9,21 @@ package agent import ( - "bufio" "encoding/json" "errors" - "fmt" "io" "net/http" - "os" - "path" "sort" "time" - "github.com/DataDog/zstd" "github.com/gorilla/mux" "github.com/DataDog/datadog-agent/cmd/agent/common" "github.com/DataDog/datadog-agent/cmd/agent/common/signals" - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" "github.com/DataDog/datadog-agent/comp/api/api" "github.com/DataDog/datadog-agent/comp/api/api/utils" + streamutils "github.com/DataDog/datadog-agent/comp/api/api/utils/stream" "github.com/DataDog/datadog-agent/comp/collector/collector" "github.com/DataDog/datadog-agent/comp/core/autodiscovery" "github.com/DataDog/datadog-agent/comp/core/gui" @@ -36,15 +31,10 @@ import ( "github.com/DataDog/datadog-agent/comp/core/status" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" - dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" - dogstatsddebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" - "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" "github.com/DataDog/datadog-agent/pkg/aggregator/sender" - "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/diagnose" "github.com/DataDog/datadog-agent/pkg/diagnose/diagnosis" - "github.com/DataDog/datadog-agent/pkg/logs/diagnostic" "github.com/DataDog/datadog-agent/pkg/status/health" "github.com/DataDog/datadog-agent/pkg/util/hostname" "github.com/DataDog/datadog-agent/pkg/util/log" @@ -59,16 +49,12 @@ var mimeTypeMap = map[string]string{ // SetupHandlers adds the specific handlers for /agent endpoints func SetupHandlers( r *mux.Router, - server dogstatsdServer.Component, - serverDebug dogstatsddebug.Component, wmeta workloadmeta.Component, logsAgent optional.Option[logsAgent.Component], senderManager sender.DiagnoseSenderManager, - demux demultiplexer.Component, secretResolver secrets.Component, statusComponent status.Component, collector optional.Option[collector.Component], - eventPlatformReceiver eventplatformreceiver.Component, ac autodiscovery.Component, gui optional.Option[gui.Component], providers []api.EndpointProvider, @@ -87,7 +73,6 @@ func SetupHandlers( r.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { getStatus(w, r, statusComponent, "") }).Methods("GET") - r.HandleFunc("/stream-event-platform", streamEventPlatform(eventPlatformReceiver)).Methods("POST") r.HandleFunc("/status/health", getHealth).Methods("GET") r.HandleFunc("/{component}/status", func(w http.ResponseWriter, r *http.Request) { componentStatusGetterHandler(w, r, statusComponent) }).Methods("GET") r.HandleFunc("/{component}/status", componentStatusHandler).Methods("POST") @@ -100,12 +85,6 @@ func SetupHandlers( getDiagnose(w, r, diagnoseDeps) }).Methods("POST") - r.HandleFunc("/dogstatsd-contexts-dump", func(w http.ResponseWriter, r *http.Request) { dumpDogstatsdContexts(w, r, demux) }).Methods("POST") - // Some agent subcommands do not provide these dependencies (such as JMX) - if server != nil && serverDebug != nil { - r.HandleFunc("/dogstatsd-stats", func(w http.ResponseWriter, r *http.Request) { getDogstatsdStats(w, r, server, serverDebug) }).Methods("GET") - } - if logsAgent, ok := logsAgent.Get(); ok { r.HandleFunc("/stream-logs", streamLogs(logsAgent)).Methods("POST") } @@ -200,128 +179,9 @@ func getStatus(w http.ResponseWriter, r *http.Request, statusComponent status.Co w.Write(s) } +// TODO: logsAgent is a module so have to make the api component a module too func streamLogs(logsAgent logsAgent.Component) func(w http.ResponseWriter, r *http.Request) { - return getStreamFunc(func() messageReceiver { return logsAgent.GetMessageReceiver() }, "logs", "logs agent") -} - -func streamEventPlatform(eventPlatformReceiver eventplatformreceiver.Component) func(w http.ResponseWriter, r *http.Request) { - return getStreamFunc(func() messageReceiver { return eventPlatformReceiver }, "event platform payloads", "agent") -} - -type messageReceiver interface { - SetEnabled(e bool) bool - Filter(filters *diagnostic.Filters, done <-chan struct{}) <-chan string -} - -func getStreamFunc(messageReceiverFunc func() messageReceiver, streamType, agentType string) func(w http.ResponseWriter, r *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - log.Infof("Got a request to stream %s.", streamType) - w.Header().Set("Transfer-Encoding", "chunked") - - messageReceiver := messageReceiverFunc() - - flusher, ok := w.(http.Flusher) - if !ok { - log.Errorf("Expected a Flusher type, got: %v", w) - return - } - - if messageReceiver == nil { - http.Error(w, fmt.Sprintf("The %s is not running", agentType), 405) - flusher.Flush() - log.Infof("The %s is not running - can't stream %s", agentType, streamType) - return - } - - if !messageReceiver.SetEnabled(true) { - http.Error(w, fmt.Sprintf("Another client is already streaming %s.", streamType), 405) - flusher.Flush() - log.Infof("%s are already streaming. Dropping connection.", streamType) - return - } - defer messageReceiver.SetEnabled(false) - - var filters diagnostic.Filters - - if r.Body != http.NoBody { - body, err := io.ReadAll(r.Body) - if err != nil { - http.Error(w, log.Errorf("Error while reading HTTP request body: %s", err).Error(), 500) - return - } - - if err := json.Unmarshal(body, &filters); err != nil { - http.Error(w, log.Errorf("Error while unmarshaling JSON from request body: %s", err).Error(), 500) - return - } - } - - // Reset the `server_timeout` deadline for this connection as streaming holds the connection open. - conn := utils.GetConnection(r) - _ = conn.SetDeadline(time.Time{}) - - done := make(chan struct{}) - defer close(done) - logChan := messageReceiver.Filter(&filters, done) - flushTimer := time.NewTicker(time.Second) - for { - // Handlers for detecting a closed connection (from either the server or client) - select { - case <-w.(http.CloseNotifier).CloseNotify(): //nolint - return - case <-r.Context().Done(): - return - case line := <-logChan: - fmt.Fprint(w, line) - case <-flushTimer.C: - // The buffer will flush on its own most of the time, but when we run out of logs flush so the client is up to date. - flusher.Flush() - } - } - } -} - -func getDogstatsdStats(w http.ResponseWriter, _ *http.Request, dogstatsdServer dogstatsdServer.Component, serverDebug dogstatsddebug.Component) { - log.Info("Got a request for the Dogstatsd stats.") - - if !config.Datadog.GetBool("use_dogstatsd") { - w.Header().Set("Content-Type", "application/json") - body, _ := json.Marshal(map[string]string{ - "error": "Dogstatsd not enabled in the Agent configuration", - "error_type": "no server", - }) - w.WriteHeader(400) - w.Write(body) - return - } - - if !config.Datadog.GetBool("dogstatsd_metrics_stats_enable") { - w.Header().Set("Content-Type", "application/json") - body, _ := json.Marshal(map[string]string{ - "error": "Dogstatsd metrics stats not enabled in the Agent configuration", - "error_type": "not enabled", - }) - w.WriteHeader(400) - w.Write(body) - return - } - - // Weird state that should not happen: dogstatsd is enabled - // but the server has not been successfully initialized. - // Return no data. - if !dogstatsdServer.IsRunning() { - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{}`)) - return - } - - jsonStats, err := serverDebug.GetJSONDebugStats() - if err != nil { - utils.SetJSONError(w, log.Errorf("Error getting marshalled Dogstatsd stats: %s", err), 500) - return - } - - w.Write(jsonStats) + return streamutils.GetStreamFunc(func() streamutils.MessageReceiver { return logsAgent.GetMessageReceiver() }, "logs", "logs agent") } func getHealth(w http.ResponseWriter, _ *http.Request) { @@ -415,46 +275,3 @@ func getDiagnose(w http.ResponseWriter, r *http.Request, diagnoseDeps diagnose.S utils.SetJSONError(w, log.Errorf("Unable to marshal config check response: %s", err), 500) } } - -func dumpDogstatsdContexts(w http.ResponseWriter, _ *http.Request, demux demultiplexer.Component) { - if demux == nil { - utils.SetJSONError(w, log.Errorf("Unable to stream dogstatsd contexts, demultiplexer is not initialized"), 404) - return - } - - path, err := dumpDogstatsdContextsImpl(demux) - if err != nil { - utils.SetJSONError(w, log.Errorf("Failed to create dogstatsd contexts dump: %v", err), 500) - return - } - - resp, err := json.Marshal(path) - if err != nil { - utils.SetJSONError(w, log.Errorf("Failed to serialize response: %v", err), 500) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Write(resp) -} - -func dumpDogstatsdContextsImpl(demux demultiplexer.Component) (string, error) { - path := path.Join(config.Datadog.GetString("run_path"), "dogstatsd_contexts.json.zstd") - - f, err := os.Create(path) - if err != nil { - return "", err - } - - c := zstd.NewWriter(f) - - w := bufio.NewWriter(c) - - for _, err := range []error{demux.DumpDogstatsdContexts(w), w.Flush(), c.Close(), f.Close()} { - if err != nil { - return "", err - } - } - - return path, nil -} diff --git a/comp/api/api/apiimpl/internal/agent/agent_test.go b/comp/api/api/apiimpl/internal/agent/agent_test.go index 44f28079f8847b..474276bf6a70bc 100644 --- a/comp/api/api/apiimpl/internal/agent/agent_test.go +++ b/comp/api/api/apiimpl/internal/agent/agent_test.go @@ -18,6 +18,7 @@ import ( "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer/demultiplexerimpl" + demultiplexerendpointmock "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexerendpoint/fx-mock" "github.com/DataDog/datadog-agent/comp/api/api" "github.com/DataDog/datadog-agent/comp/collector/collector" "github.com/DataDog/datadog-agent/comp/core/autodiscovery" @@ -98,6 +99,7 @@ func getComponentDeps(t *testing.T) handlerdeps { hostimpl.MockModule(), inventoryagentimpl.MockModule(), demultiplexerimpl.MockModule(), + demultiplexerendpointmock.MockModule(), inventoryhostimpl.MockModule(), secretsimpl.MockModule(), fx.Provide(func(secretMock secrets.Mock) secrets.Component { @@ -128,16 +130,12 @@ func setupRoutes(t *testing.T) *mux.Router { router := mux.NewRouter() SetupHandlers( router, - deps.Server, - deps.ServerDebug, deps.Wmeta, deps.LogsAgent, sender, - deps.Demux, deps.SecretResolver, deps.StatusComponent, deps.Collector, - deps.EventPlatformReceiver, deps.Ac, deps.Gui, deps.EndpointProviders, @@ -162,6 +160,21 @@ func TestSetupHandlers(t *testing.T) { method: "POST", wantCode: 200, }, + { + route: "/stream-event-platform", + method: "POST", + wantCode: 200, + }, + { + route: "/dogstatsd-contexts-dump", + method: "POST", + wantCode: 200, + }, + { + route: "/dogstatsd-stats", + method: "GET", + wantCode: 200, + }, { route: "/config", method: "GET", diff --git a/comp/api/api/apiimpl/server.go b/comp/api/api/apiimpl/server.go index e9fb32bf163fbf..59ecf051dad52a 100644 --- a/comp/api/api/apiimpl/server.go +++ b/comp/api/api/apiimpl/server.go @@ -14,7 +14,6 @@ import ( "github.com/cihub/seelog" - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" "github.com/DataDog/datadog-agent/comp/api/api" "github.com/DataDog/datadog-agent/comp/collector/collector" "github.com/DataDog/datadog-agent/comp/core/autodiscovery" @@ -26,8 +25,6 @@ import ( "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" - dogstatsddebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" - "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" "github.com/DataDog/datadog-agent/comp/remote-config/rcservice" "github.com/DataDog/datadog-agent/comp/remote-config/rcservicemrf" @@ -67,16 +64,13 @@ func StartServers( dogstatsdServer dogstatsdServer.Component, capture replay.Component, pidMap pidmap.Component, - serverDebug dogstatsddebug.Component, wmeta workloadmeta.Component, taggerComp tagger.Component, logsAgent optional.Option[logsAgent.Component], senderManager sender.DiagnoseSenderManager, - demux demultiplexer.Component, secretResolver secrets.Component, statusComponent status.Component, collector optional.Option[collector.Component], - eventPlatformReceiver eventplatformreceiver.Component, ac autodiscovery.Component, gui optional.Option[gui.Component], providers []api.EndpointProvider, @@ -114,16 +108,13 @@ func StartServers( dogstatsdServer, capture, pidMap, - serverDebug, wmeta, taggerComp, logsAgent, senderManager, - demux, secretResolver, statusComponent, collector, - eventPlatformReceiver, ac, gui, providers, diff --git a/comp/api/api/apiimpl/server_cmd.go b/comp/api/api/apiimpl/server_cmd.go index 93ba9867790d55..2f035e689f637a 100644 --- a/comp/api/api/apiimpl/server_cmd.go +++ b/comp/api/api/apiimpl/server_cmd.go @@ -20,7 +20,6 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials" - "github.com/DataDog/datadog-agent/comp/aggregator/demultiplexer" "github.com/DataDog/datadog-agent/comp/api/api" "github.com/DataDog/datadog-agent/comp/api/api/apiimpl/internal/agent" "github.com/DataDog/datadog-agent/comp/api/api/apiimpl/internal/check" @@ -37,8 +36,6 @@ import ( "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" - dogstatsddebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" - "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" "github.com/DataDog/datadog-agent/comp/remote-config/rcservice" "github.com/DataDog/datadog-agent/comp/remote-config/rcservicemrf" @@ -62,16 +59,13 @@ func startCMDServer( dogstatsdServer dogstatsdServer.Component, capture replay.Component, pidMap pidmap.Component, - serverDebug dogstatsddebug.Component, wmeta workloadmeta.Component, taggerComp tagger.Component, logsAgent optional.Option[logsAgent.Component], senderManager sender.DiagnoseSenderManager, - demux demultiplexer.Component, secretResolver secrets.Component, statusComponent status.Component, collector optional.Option[collector.Component], - eventPlatformReceiver eventplatformreceiver.Component, ac autodiscovery.Component, gui optional.Option[gui.Component], providers []api.EndpointProvider, @@ -141,16 +135,12 @@ func startCMDServer( http.StripPrefix("/agent", agent.SetupHandlers( agentMux, - dogstatsdServer, - serverDebug, wmeta, logsAgent, senderManager, - demux, secretResolver, statusComponent, collector, - eventPlatformReceiver, ac, gui, providers, diff --git a/comp/api/api/utils/stream/stream.go b/comp/api/api/utils/stream/stream.go new file mode 100644 index 00000000000000..416890d9ffef40 --- /dev/null +++ b/comp/api/api/utils/stream/stream.go @@ -0,0 +1,94 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2023-present Datadog, Inc. + +// Package stream has api stream utility methods that components can use for directing output to a stream receiver +package stream + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + apiutils "github.com/DataDog/datadog-agent/comp/api/api/utils" + "github.com/DataDog/datadog-agent/pkg/logs/diagnostic" + "github.com/DataDog/datadog-agent/pkg/util/log" +) + +// MessageReceiver is an exported interface for a valid receiver of streamed output +type MessageReceiver interface { + SetEnabled(e bool) bool + Filter(filters *diagnostic.Filters, done <-chan struct{}) <-chan string +} + +// GetStreamFunc returns a handlerfunc that handles request to stream output to the desired receiver +func GetStreamFunc(messageReceiverFunc func() MessageReceiver, streamType, agentType string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + log.Infof("Got a request to stream %s.", streamType) + w.Header().Set("Transfer-Encoding", "chunked") + + messageReceiver := messageReceiverFunc() + + flusher, ok := w.(http.Flusher) + if !ok { + log.Errorf("Expected a Flusher type, got: %v", w) + return + } + + if messageReceiver == nil { + http.Error(w, fmt.Sprintf("The %s is not running", agentType), 405) + flusher.Flush() + log.Infof("The %s is not running - can't stream %s", agentType, streamType) + return + } + + if !messageReceiver.SetEnabled(true) { + http.Error(w, fmt.Sprintf("Another client is already streaming %s.", streamType), 405) + flusher.Flush() + log.Infof("%s are already streaming. Dropping connection.", streamType) + return + } + defer messageReceiver.SetEnabled(false) + + var filters diagnostic.Filters + + if r.Body != http.NoBody { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, log.Errorf("Error while reading HTTP request body: %s", err).Error(), 500) + return + } + + if err := json.Unmarshal(body, &filters); err != nil { + http.Error(w, log.Errorf("Error while unmarshaling JSON from request body: %s", err).Error(), 500) + return + } + } + + // Reset the `server_timeout` deadline for this connection as streaming holds the connection open. + conn := apiutils.GetConnection(r) + _ = conn.SetDeadline(time.Time{}) + + done := make(chan struct{}) + defer close(done) + logChan := messageReceiver.Filter(&filters, done) + flushTimer := time.NewTicker(time.Second) + for { + // Handlers for detecting a closed connection (from either the server or client) + select { + case <-w.(http.CloseNotifier).CloseNotify(): //nolint + return + case <-r.Context().Done(): + return + case line := <-logChan: + fmt.Fprint(w, line) + case <-flushTimer.C: + // The buffer will flush on its own most of the time, but when we run out of logs flush so the client is up to date. + flusher.Flush() + } + } + } +} diff --git a/comp/dogstatsd/server/server.go b/comp/dogstatsd/server/server.go index a83597902aa5e9..4fdf79592afbd5 100644 --- a/comp/dogstatsd/server/server.go +++ b/comp/dogstatsd/server/server.go @@ -17,6 +17,7 @@ import ( "go.uber.org/fx" + "github.com/DataDog/datadog-agent/comp/api/api" configComponent "github.com/DataDog/datadog-agent/comp/core/config" logComponent "github.com/DataDog/datadog-agent/comp/core/log" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" @@ -81,6 +82,13 @@ type dependencies struct { WMeta optional.Option[workloadmeta.Component] } +type provides struct { + fx.Out + + Comp Component + StatsEndpoint api.AgentEndpointProvider +} + // When the internal telemetry is enabled, used to tag the origin // on the processed metric. type cachedOriginCounter struct { @@ -198,17 +206,20 @@ func initTelemetry(cfg config.Reader, logger logComponent.Component) { } // TODO: (components) - merge with newServerCompat once NewServerlessServer is removed -func newServer(deps dependencies) Component { +func newServer(deps dependencies) provides { s := newServerCompat(deps.Config, deps.Log, deps.Replay, deps.Debug, deps.Params.Serverless, deps.Demultiplexer, deps.WMeta, deps.PidMap) - if config.Datadog.GetBool("use_dogstatsd") { + if deps.Config.GetBool("use_dogstatsd") { deps.Lc.Append(fx.Hook{ OnStart: s.startHook, OnStop: s.stop, }) } - return s + return provides{ + Comp: s, + StatsEndpoint: api.NewAgentEndpointProvider(s.writeStats, "/dogstatsd-stats", "GET"), + } } diff --git a/comp/dogstatsd/server/server_mock.go b/comp/dogstatsd/server/server_mock.go index dbbc3269f02829..1a552755b85c76 100644 --- a/comp/dogstatsd/server/server_mock.go +++ b/comp/dogstatsd/server/server_mock.go @@ -6,17 +6,37 @@ package server import ( + "net/http" "time" + "github.com/DataDog/datadog-agent/comp/api/api" "github.com/DataDog/datadog-agent/pkg/aggregator" + "go.uber.org/fx" ) type serverMock struct { isRunning bool } -func newMock() Component { - return &serverMock{} +// MockProvides is the mock component output +type MockProvides struct { + fx.Out + + Comp Component + Endpoint api.AgentEndpointProvider +} + +func newMock() MockProvides { + m := &serverMock{} + return MockProvides{ + Comp: m, + Endpoint: api.NewAgentEndpointProvider(m.handlerFunc, "/dogstatsd-stats", "GET"), + } +} + +// ServeHTTP is a simple mocked http.Handler function +func (s *serverMock) handlerFunc(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("OK")) } //nolint:revive // TODO(AML) Fix revive linter @@ -25,10 +45,12 @@ func (s *serverMock) Start(demultiplexer aggregator.Demultiplexer) error { return nil } +// Stop is a mocked function that flips isRunning to false func (s *serverMock) Stop() { s.isRunning = false } +// IsRunning is a mocked function that returns whether the mock was set to running func (s *serverMock) IsRunning() bool { return s.isRunning } @@ -38,14 +60,17 @@ func (s *serverMock) Capture(p string, d time.Duration, compressed bool) (string return "", nil } +// UdsListenerRunning is a mocked function that returns false func (s *serverMock) UdsListenerRunning() bool { return false } +// UDPLocalAddr is a mocked function but UDP isn't enabled on the mock func (s *serverMock) UDPLocalAddr() string { return "" } +// ServerlessFlush is a noop mocked function func (s *serverMock) ServerlessFlush(time.Duration) {} //nolint:revive // TODO(AML) Fix revive linter diff --git a/comp/dogstatsd/server/stats_endpoint.go b/comp/dogstatsd/server/stats_endpoint.go new file mode 100644 index 00000000000000..85401e85c99e85 --- /dev/null +++ b/comp/dogstatsd/server/stats_endpoint.go @@ -0,0 +1,56 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2023-present Datadog, Inc. + +package server + +import ( + "encoding/json" + "net/http" + + "github.com/DataDog/datadog-agent/comp/api/api/utils" +) + +func (s *server) writeStats(w http.ResponseWriter, _ *http.Request) { + s.log.Info("Got a request for the Dogstatsd stats.") + + if !s.config.GetBool("use_dogstatsd") { + w.Header().Set("Content-Type", "application/json") + body, _ := json.Marshal(map[string]string{ + "error": "Dogstatsd not enabled in the Agent configuration", + "error_type": "no server", + }) + w.WriteHeader(400) + w.Write(body) + return + } + + if !s.config.GetBool("dogstatsd_metrics_stats_enable") { + w.Header().Set("Content-Type", "application/json") + body, _ := json.Marshal(map[string]string{ + "error": "Dogstatsd metrics stats not enabled in the Agent configuration", + "error_type": "not enabled", + }) + w.WriteHeader(400) + w.Write(body) + return + } + + // Weird state that should not happen: dogstatsd is enabled + // but the server has not been successfully initialized. + // Return no data. + if !s.IsRunning() { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{}`)) + return + } + + jsonStats, err := s.Debug.GetJSONDebugStats() + if err != nil { + utils.SetJSONError(w, s.log.Errorf("Error getting marshalled Dogstatsd stats: %s", err), 500) + return + } + + w.Write(jsonStats) +} diff --git a/comp/forwarder/eventplatform/eventplatformimpl/epforwarder.go b/comp/forwarder/eventplatform/eventplatformimpl/epforwarder.go index 56442a1bd97ff9..832f4b287336dc 100644 --- a/comp/forwarder/eventplatform/eventplatformimpl/epforwarder.go +++ b/comp/forwarder/eventplatform/eventplatformimpl/epforwarder.go @@ -521,7 +521,7 @@ func NewNoopEventPlatformForwarder(hostname hostnameinterface.Component) eventpl } func newNoopEventPlatformForwarder(hostname hostnameinterface.Component) *defaultEventPlatformForwarder { - f := newDefaultEventPlatformForwarder(pkgconfig.Datadog, eventplatformreceiverimpl.NewReceiver(hostname)) + f := newDefaultEventPlatformForwarder(pkgconfig.Datadog, eventplatformreceiverimpl.NewReceiver(hostname).Comp) // remove the senders for _, p := range f.pipelines { p.strategy = nil diff --git a/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl/eventplatformreceiver.go b/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl/eventplatformreceiver.go index 55615ced77d856..071974c93a7109 100644 --- a/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl/eventplatformreceiver.go +++ b/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl/eventplatformreceiver.go @@ -7,8 +7,12 @@ package eventplatformreceiverimpl import ( + "net/http" + "go.uber.org/fx" + "github.com/DataDog/datadog-agent/comp/api/api" + apiutils "github.com/DataDog/datadog-agent/comp/api/api/utils/stream" "github.com/DataDog/datadog-agent/comp/core/hostname/hostnameinterface" "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver" "github.com/DataDog/datadog-agent/pkg/logs/diagnostic" @@ -22,7 +26,22 @@ func Module() fxutil.Module { ) } +type provides struct { + fx.Out + + Comp eventplatformreceiver.Component + Endpoint api.AgentEndpointProvider +} + +func streamEventPlatform(eventPlatformReceiver eventplatformreceiver.Component) func(w http.ResponseWriter, r *http.Request) { + return apiutils.GetStreamFunc(func() apiutils.MessageReceiver { return eventPlatformReceiver }, "event platform payloads", "agent") +} + // NewReceiver returns a new event platform receiver. -func NewReceiver(hostname hostnameinterface.Component) eventplatformreceiver.Component { - return diagnostic.NewBufferedMessageReceiver(&epFormatter{}, hostname) +func NewReceiver(hostname hostnameinterface.Component) provides { // nolint:revive + epr := diagnostic.NewBufferedMessageReceiver(&epFormatter{}, hostname) + return provides{ + Comp: epr, + Endpoint: api.NewAgentEndpointProvider(streamEventPlatform(epr), "/stream-event-platform", "POST"), + } } diff --git a/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl/mock.go b/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl/mock.go index d4836cd721d05b..c43d2d32e9451a 100644 --- a/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl/mock.go +++ b/comp/forwarder/eventplatformreceiver/eventplatformreceiverimpl/mock.go @@ -8,6 +8,9 @@ package eventplatformreceiverimpl import ( + "net/http" + + "github.com/DataDog/datadog-agent/comp/api/api" eprinterface "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver" "github.com/DataDog/datadog-agent/pkg/logs/diagnostic" "github.com/DataDog/datadog-agent/pkg/logs/message" @@ -21,6 +24,14 @@ func MockModule() fxutil.Module { fx.Provide(newMock)) } +// MockProvides is the mock component output +type MockProvides struct { + fx.Out + + Comp eprinterface.Component + Endpoint api.AgentEndpointProvider +} + // MockEventPlatformReceiver is the mocked struct that implements the eventplatformreceiver interface type MockEventPlatformReceiver struct{} @@ -44,7 +55,16 @@ func (epr *MockEventPlatformReceiver) Filter(_ *diagnostic.Filters, _ <-chan str return c } +// handlerFunc is a simple mocked http.Handler function +func (epr *MockEventPlatformReceiver) handlerFunc(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte("OK")) +} + // newMock returns the mocked eventplatformreceiver struct -func newMock() eprinterface.Component { - return &MockEventPlatformReceiver{} +func newMock() MockProvides { + epr := &MockEventPlatformReceiver{} + return MockProvides{ + Comp: epr, + Endpoint: api.NewAgentEndpointProvider(epr.handlerFunc, "/stream-event-platform", "POST"), + } } From a99881ed5b6876c6c8df297d7b996a6ea594f21b Mon Sep 17 00:00:00 2001 From: Baptiste Foy Date: Fri, 24 May 2024 15:23:19 +0200 Subject: [PATCH 24/32] fix(installer): Set up sockets at injector install (#25890) --- pkg/fleet/installer/default_packages_test.go | 6 ++++++ pkg/fleet/installer/service/apm_inject.go | 18 ++++++++++++++++++ .../installer/service/datadog_installer.go | 17 ----------------- .../tests/installer/package_apm_inject_test.go | 1 + .../tests/installer/package_installer_test.go | 1 - 5 files changed, 25 insertions(+), 18 deletions(-) diff --git a/pkg/fleet/installer/default_packages_test.go b/pkg/fleet/installer/default_packages_test.go index e1c52b3c27bba3..f1b96de7206bea 100644 --- a/pkg/fleet/installer/default_packages_test.go +++ b/pkg/fleet/installer/default_packages_test.go @@ -94,6 +94,12 @@ func TestDefaultPackages(t *testing.T) { env: &env.Env{DefaultPackagesInstallOverride: map[string]bool{"datadog-agent": true}, DefaultPackagesVersionOverride: map[string]string{"datadog-agent": "1.2.3"}}, expected: []pkg{{n: "datadog-agent", v: "1.2.3"}}, }, + { + name: "APM inject before agent", + packages: []defaultPackage{{name: "datadog-apm-inject", released: true}, {name: "datadog-agent", released: true}}, + env: &env.Env{}, + expected: []pkg{{n: "datadog-apm-inject", v: "latest"}, {n: "datadog-agent", v: "latest"}}, + }, } for _, tt := range tests { diff --git a/pkg/fleet/installer/service/apm_inject.go b/pkg/fleet/installer/service/apm_inject.go index addabd0dc3204a..aa4a735ebab81a 100644 --- a/pkg/fleet/installer/service/apm_inject.go +++ b/pkg/fleet/installer/service/apm_inject.go @@ -99,6 +99,24 @@ func (a *apmInjectorInstaller) Setup(ctx context.Context) (err error) { if err := a.verifyDockerRuntime(); err != nil { return err } + + // Set up defaults for agent sockets + if err = configureSocketsEnv(); err != nil { + return + } + if err = addSystemDEnvOverrides(agentUnit); err != nil { + return + } + if err = addSystemDEnvOverrides(agentExp); err != nil { + return + } + if err = addSystemDEnvOverrides(traceAgentUnit); err != nil { + return + } + if err = addSystemDEnvOverrides(traceAgentExp); err != nil { + return + } + return nil } diff --git a/pkg/fleet/installer/service/datadog_installer.go b/pkg/fleet/installer/service/datadog_installer.go index f3319a587cd28c..1dd0533b96761e 100644 --- a/pkg/fleet/installer/service/datadog_installer.go +++ b/pkg/fleet/installer/service/datadog_installer.go @@ -109,23 +109,6 @@ func SetupInstaller(ctx context.Context) (err error) { return fmt.Errorf("error creating symlink to /usr/bin/datadog-installer: %w", err) } - // Set up defaults for packages interacting with each other - if err = configureSocketsEnv(); err != nil { - return - } - if err = addSystemDEnvOverrides(agentUnit); err != nil { - return - } - if err = addSystemDEnvOverrides(agentExp); err != nil { - return - } - if err = addSystemDEnvOverrides(traceAgentUnit); err != nil { - return - } - if err = addSystemDEnvOverrides(traceAgentExp); err != nil { - return - } - // FIXME(Arthur): enable the daemon unit by default and use the same strategy as the system probe if os.Getenv("DD_REMOTE_UPDATES") != "true" { return nil diff --git a/test/new-e2e/tests/installer/package_apm_inject_test.go b/test/new-e2e/tests/installer/package_apm_inject_test.go index ba8f0beece5ebe..a4b6fadae3a40c 100644 --- a/test/new-e2e/tests/installer/package_apm_inject_test.go +++ b/test/new-e2e/tests/installer/package_apm_inject_test.go @@ -38,6 +38,7 @@ func (s *packageApmInjectSuite) TestInstall() { s.host.AssertPackageInstalledByInstaller("datadog-agent", "datadog-apm-inject", "datadog-apm-library-python") s.host.AssertPackageNotInstalledByPackageManager("datadog-agent", "datadog-apm-inject", "datadog-apm-library-python") state := s.host.State() + state.AssertFileExists("/var/run/datadog/installer/environment", 0644, "root", "root") state.AssertFileExists("/etc/ld.so.preload", 0644, "root", "root") s.assertLDPreloadInstrumented() s.assertDockerdInstrumented() diff --git a/test/new-e2e/tests/installer/package_installer_test.go b/test/new-e2e/tests/installer/package_installer_test.go index 396aea99c2dbd0..9bfab9da3745c2 100644 --- a/test/new-e2e/tests/installer/package_installer_test.go +++ b/test/new-e2e/tests/installer/package_installer_test.go @@ -39,7 +39,6 @@ func (s *packageInstallerSuite) TestInstall() { state.AssertDirExists("/var/run/datadog", 0755, "dd-agent", "dd-agent") state.AssertDirExists("/var/run/datadog/installer", 0755, "dd-agent", "dd-agent") state.AssertDirExists("/var/run/datadog/installer/locks", 0777, "root", "root") - state.AssertFileExists("/var/run/datadog/installer/environment", 0644, "root", "root") state.AssertDirExists("/opt/datadog-installer", 0755, "root", "root") state.AssertDirExists("/opt/datadog-packages", 0755, "root", "root") From b975a87dcea94afb2de845f9e12d66a8ef7d683b Mon Sep 17 00:00:00 2001 From: Paul Cacheux Date: Fri, 24 May 2024 15:48:20 +0200 Subject: [PATCH 25/32] [CWS] ensure imds is disabled if network is disabled or network ingress is disabled (#25893) --- pkg/security/probe/probe_ebpf.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/security/probe/probe_ebpf.go b/pkg/security/probe/probe_ebpf.go index 53e2f88eff770e..fb25e632de5b77 100644 --- a/pkg/security/probe/probe_ebpf.go +++ b/pkg/security/probe/probe_ebpf.go @@ -1113,6 +1113,9 @@ func (p *EBPFProbe) validEventTypeForConfig(eventType string) bool { if eventType == "dns" && !p.config.Probe.NetworkEnabled { return false } + if eventType == "imds" && (!p.config.Probe.NetworkEnabled || !p.config.Probe.NetworkIngressEnabled) { + return false + } return true } From 32d6911ac5625f2ebcaf79d2bdffb606338712a3 Mon Sep 17 00:00:00 2001 From: David Ortiz Date: Fri, 24 May 2024 15:48:27 +0200 Subject: [PATCH 26/32] [workloadmeta,tagger] Move proto utils (#25879) * [workloadmeta] Move proto utils to workloadmeta dir * [tagger] Move proto utils to tagger dir * [serverless] Update list of dependencies --- cmd/serverless/linux_dependencies_amd64.txt | 2 +- cmd/serverless/linux_dependencies_arm64.txt | 2 +- .../proto/tagger.go => comp/core/tagger/proto/proto.go | 2 +- comp/core/tagger/taggerimpl/replay/tagger.go | 4 ++-- comp/core/tagger/taggerimpl/server/server.go | 8 ++++---- .../internal/remote/workloadmeta/workloadmeta.go | 4 ++-- .../internal/remote/workloadmeta/workloadmeta_test.go | 2 +- .../core/workloadmeta/proto/proto.go | 1 + .../core/workloadmeta/proto/proto_test.go | 0 comp/core/workloadmeta/server/server.go | 6 +++--- comp/dogstatsd/replay/writer.go | 5 ++--- 11 files changed, 18 insertions(+), 18 deletions(-) rename pkg/util/proto/tagger.go => comp/core/tagger/proto/proto.go (97%) rename pkg/util/proto/workloadmeta.go => comp/core/workloadmeta/proto/proto.go (99%) rename pkg/util/proto/workloadmeta_test.go => comp/core/workloadmeta/proto/proto_test.go (100%) diff --git a/cmd/serverless/linux_dependencies_amd64.txt b/cmd/serverless/linux_dependencies_amd64.txt index 40098ef80e7943..c0588d00a21453 100644 --- a/cmd/serverless/linux_dependencies_amd64.txt +++ b/cmd/serverless/linux_dependencies_amd64.txt @@ -78,6 +78,7 @@ github.com/DataDog/datadog-agent/comp/core/log/logimpl github.com/DataDog/datadog-agent/comp/core/secrets github.com/DataDog/datadog-agent/comp/core/status github.com/DataDog/datadog-agent/comp/core/tagger +github.com/DataDog/datadog-agent/comp/core/tagger/proto github.com/DataDog/datadog-agent/comp/core/tagger/types github.com/DataDog/datadog-agent/comp/core/tagger/utils github.com/DataDog/datadog-agent/comp/core/telemetry @@ -262,7 +263,6 @@ github.com/DataDog/datadog-agent/pkg/util/log github.com/DataDog/datadog-agent/pkg/util/log/zap github.com/DataDog/datadog-agent/pkg/util/optional github.com/DataDog/datadog-agent/pkg/util/pointer -github.com/DataDog/datadog-agent/pkg/util/proto github.com/DataDog/datadog-agent/pkg/util/retry github.com/DataDog/datadog-agent/pkg/util/scrubber github.com/DataDog/datadog-agent/pkg/util/sort diff --git a/cmd/serverless/linux_dependencies_arm64.txt b/cmd/serverless/linux_dependencies_arm64.txt index 837762b8d0f8ea..6160db8295d3bc 100644 --- a/cmd/serverless/linux_dependencies_arm64.txt +++ b/cmd/serverless/linux_dependencies_arm64.txt @@ -78,6 +78,7 @@ github.com/DataDog/datadog-agent/comp/core/log/logimpl github.com/DataDog/datadog-agent/comp/core/secrets github.com/DataDog/datadog-agent/comp/core/status github.com/DataDog/datadog-agent/comp/core/tagger +github.com/DataDog/datadog-agent/comp/core/tagger/proto github.com/DataDog/datadog-agent/comp/core/tagger/types github.com/DataDog/datadog-agent/comp/core/tagger/utils github.com/DataDog/datadog-agent/comp/core/telemetry @@ -262,7 +263,6 @@ github.com/DataDog/datadog-agent/pkg/util/log github.com/DataDog/datadog-agent/pkg/util/log/zap github.com/DataDog/datadog-agent/pkg/util/optional github.com/DataDog/datadog-agent/pkg/util/pointer -github.com/DataDog/datadog-agent/pkg/util/proto github.com/DataDog/datadog-agent/pkg/util/retry github.com/DataDog/datadog-agent/pkg/util/scrubber github.com/DataDog/datadog-agent/pkg/util/sort diff --git a/pkg/util/proto/tagger.go b/comp/core/tagger/proto/proto.go similarity index 97% rename from pkg/util/proto/tagger.go rename to comp/core/tagger/proto/proto.go index 981616d400b6d7..139ca918f3a05c 100644 --- a/pkg/util/proto/tagger.go +++ b/comp/core/tagger/proto/proto.go @@ -3,7 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-2020 Datadog, Inc. -// Package proto contains protobuf related helpers. +// Package proto provides conversions between Tagger types and protobuf. package proto import ( diff --git a/comp/core/tagger/taggerimpl/replay/tagger.go b/comp/core/tagger/taggerimpl/replay/tagger.go index e0c4a816897679..ce14dd0d4404bb 100644 --- a/comp/core/tagger/taggerimpl/replay/tagger.go +++ b/comp/core/tagger/taggerimpl/replay/tagger.go @@ -10,6 +10,7 @@ import ( "context" "time" + "github.com/DataDog/datadog-agent/comp/core/tagger/proto" "github.com/DataDog/datadog-agent/comp/core/tagger/taggerimpl/empty" "github.com/DataDog/datadog-agent/comp/core/tagger/taggerimpl/tagstore" "github.com/DataDog/datadog-agent/comp/core/tagger/taggerimpl/telemetry" @@ -17,7 +18,6 @@ import ( pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" "github.com/DataDog/datadog-agent/pkg/tagset" "github.com/DataDog/datadog-agent/pkg/util/log" - pbutils "github.com/DataDog/datadog-agent/pkg/util/proto" ) // Tagger stores tags to entity as stored in a replay state. @@ -115,7 +115,7 @@ func (t *Tagger) LoadState(state map[string]*pb.Entity) { // better stores these as the native type for id, entity := range state { - entityID, err := pbutils.Pb2TaggerEntityID(entity.Id) + entityID, err := proto.Pb2TaggerEntityID(entity.Id) if err != nil { log.Errorf("Error getting identity ID for %v: %v", id, err) continue diff --git a/comp/core/tagger/taggerimpl/server/server.go b/comp/core/tagger/taggerimpl/server/server.go index 4b5335b6bf8960..06a89f6b04fdc5 100644 --- a/comp/core/tagger/taggerimpl/server/server.go +++ b/comp/core/tagger/taggerimpl/server/server.go @@ -15,11 +15,11 @@ import ( "google.golang.org/grpc/status" "github.com/DataDog/datadog-agent/comp/core/tagger" + "github.com/DataDog/datadog-agent/comp/core/tagger/proto" "github.com/DataDog/datadog-agent/comp/core/tagger/taggerimpl/telemetry" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" "github.com/DataDog/datadog-agent/pkg/util/grpc" "github.com/DataDog/datadog-agent/pkg/util/log" - pbutils "github.com/DataDog/datadog-agent/pkg/util/proto" ) const ( @@ -43,7 +43,7 @@ func NewServer(t tagger.Component) *Server { // and streams them to clients as pb.StreamTagsResponse events. Filtering is as // of yet not implemented. func (s *Server) TaggerStreamEntities(in *pb.StreamTagsRequest, out pb.AgentSecure_TaggerStreamEntitiesServer) error { - cardinality, err := pbutils.Pb2TaggerCardinality(in.Cardinality) + cardinality, err := proto.Pb2TaggerCardinality(in.Cardinality) if err != nil { return err } @@ -70,7 +70,7 @@ func (s *Server) TaggerStreamEntities(in *pb.StreamTagsRequest, out pb.AgentSecu responseEvents := make([]*pb.StreamTagsEvent, 0, len(events)) for _, event := range events { - e, err := pbutils.Tagger2PbEntityEvent(event) + e, err := proto.Tagger2PbEntityEvent(event) if err != nil { log.Warnf("can't convert tagger entity to protobuf: %s", err) continue @@ -127,7 +127,7 @@ func (s *Server) TaggerFetchEntity(ctx context.Context, in *pb.FetchEntityReques } entityID := fmt.Sprintf("%s://%s", in.Id.Prefix, in.Id.Uid) - cardinality, err := pbutils.Pb2TaggerCardinality(in.Cardinality) + cardinality, err := proto.Pb2TaggerCardinality(in.Cardinality) if err != nil { return nil, err } diff --git a/comp/core/workloadmeta/collectors/internal/remote/workloadmeta/workloadmeta.go b/comp/core/workloadmeta/collectors/internal/remote/workloadmeta/workloadmeta.go index c0b0f7c7465d91..c373e26aac2a63 100644 --- a/comp/core/workloadmeta/collectors/internal/remote/workloadmeta/workloadmeta.go +++ b/comp/core/workloadmeta/collectors/internal/remote/workloadmeta/workloadmeta.go @@ -16,10 +16,10 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors/internal/remote" + "github.com/DataDog/datadog-agent/comp/core/workloadmeta/proto" "github.com/DataDog/datadog-agent/pkg/config" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" grpcutil "github.com/DataDog/datadog-agent/pkg/util/grpc" - protoutils "github.com/DataDog/datadog-agent/pkg/util/proto" ) const ( @@ -102,7 +102,7 @@ func (s *streamHandler) HandleResponse(resp interface{}) ([]workloadmeta.Collect var collectorEvents []workloadmeta.CollectorEvent for _, protoEvent := range response.Events { - workloadmetaEvent, err := protoutils.WorkloadmetaEventFromProtoEvent(protoEvent) + workloadmetaEvent, err := proto.WorkloadmetaEventFromProtoEvent(protoEvent) if err != nil { return nil, err } diff --git a/comp/core/workloadmeta/collectors/internal/remote/workloadmeta/workloadmeta_test.go b/comp/core/workloadmeta/collectors/internal/remote/workloadmeta/workloadmeta_test.go index 5cc50463c5c580..7e4fe20f341557 100644 --- a/comp/core/workloadmeta/collectors/internal/remote/workloadmeta/workloadmeta_test.go +++ b/comp/core/workloadmeta/collectors/internal/remote/workloadmeta/workloadmeta_test.go @@ -25,12 +25,12 @@ import ( "github.com/DataDog/datadog-agent/comp/core" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors/internal/remote" + "github.com/DataDog/datadog-agent/comp/core/workloadmeta/proto" "github.com/DataDog/datadog-agent/comp/core/workloadmeta/server" "github.com/DataDog/datadog-agent/pkg/api/security" pkgconfig "github.com/DataDog/datadog-agent/pkg/config" pbgo "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" "github.com/DataDog/datadog-agent/pkg/util/fxutil" - "github.com/DataDog/datadog-agent/pkg/util/proto" ) type serverSecure struct { diff --git a/pkg/util/proto/workloadmeta.go b/comp/core/workloadmeta/proto/proto.go similarity index 99% rename from pkg/util/proto/workloadmeta.go rename to comp/core/workloadmeta/proto/proto.go index 772908373e499c..213f9b5ade7728 100644 --- a/pkg/util/proto/workloadmeta.go +++ b/comp/core/workloadmeta/proto/proto.go @@ -3,6 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-2020 Datadog, Inc. +// Package proto provides conversions between Workloadmeta types and protobuf. package proto import ( diff --git a/pkg/util/proto/workloadmeta_test.go b/comp/core/workloadmeta/proto/proto_test.go similarity index 100% rename from pkg/util/proto/workloadmeta_test.go rename to comp/core/workloadmeta/proto/proto_test.go diff --git a/comp/core/workloadmeta/server/server.go b/comp/core/workloadmeta/server/server.go index 1936af39209ac1..86de3ebbb1df6e 100644 --- a/comp/core/workloadmeta/server/server.go +++ b/comp/core/workloadmeta/server/server.go @@ -11,11 +11,11 @@ import ( "time" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" + "github.com/DataDog/datadog-agent/comp/core/workloadmeta/proto" "github.com/DataDog/datadog-agent/comp/core/workloadmeta/telemetry" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" "github.com/DataDog/datadog-agent/pkg/util/grpc" "github.com/DataDog/datadog-agent/pkg/util/log" - protoutils "github.com/DataDog/datadog-agent/pkg/util/proto" ) const ( @@ -37,7 +37,7 @@ type Server struct { // StreamEntities streams entities from the workloadmeta store applying the given filter func (s *Server) StreamEntities(in *pb.WorkloadmetaStreamRequest, out pb.AgentSecure_WorkloadmetaStreamEntitiesServer) error { - filter, err := protoutils.WorkloadmetaFilterFromProtoFilter(in.GetFilter()) + filter, err := proto.WorkloadmetaFilterFromProtoFilter(in.GetFilter()) if err != nil { return err } @@ -59,7 +59,7 @@ func (s *Server) StreamEntities(in *pb.WorkloadmetaStreamRequest, out pb.AgentSe protobufEvents := make([]*pb.WorkloadmetaEvent, 0, len(eventBundle.Events)) for _, event := range eventBundle.Events { - protobufEvent, err := protoutils.ProtobufEventFromWorkloadmetaEvent(event) + protobufEvent, err := proto.ProtobufEventFromWorkloadmetaEvent(event) if err != nil { log.Errorf("error converting workloadmeta event to protobuf: %s", err) diff --git a/comp/dogstatsd/replay/writer.go b/comp/dogstatsd/replay/writer.go index ffa30266b2515e..d4d83637f4d907 100644 --- a/comp/dogstatsd/replay/writer.go +++ b/comp/dogstatsd/replay/writer.go @@ -21,11 +21,10 @@ import ( "github.com/spf13/afero" "github.com/DataDog/datadog-agent/comp/core/tagger" + taggerproto "github.com/DataDog/datadog-agent/comp/core/tagger/proto" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" "github.com/DataDog/datadog-agent/pkg/util/log" - protoutils "github.com/DataDog/datadog-agent/pkg/util/proto" - "github.com/golang/protobuf/proto" ) @@ -346,7 +345,7 @@ func (tc *TrafficCaptureWriter) writeState() (int, error) { continue } - entityID, err := protoutils.Tagger2PbEntityID(entity.ID) + entityID, err := taggerproto.Tagger2PbEntityID(entity.ID) if err != nil { log.Warnf("unable to compute valid EntityID for %v", id) continue From 37030bc81b6f3d82f0a7ada71c33ac1661df9928 Mon Sep 17 00:00:00 2001 From: Alexandre Menasria <47357713+amenasria@users.noreply.github.com> Date: Fri, 24 May 2024 16:01:50 +0200 Subject: [PATCH 27/32] [Invoke Monitoring] Detect tasks run in Unit Tests (#25860) * [Invoke Monitoring] Detect tasks run in Unit Tests * Running modes -> dict * Check only INVOKE_UNIT_TESTS * Use a list instead of a dict * Catch python3 -m unittest as well * Fix get_running_modes output type * Fix typo --- tasks/custom_task/custom_task.py | 25 +++++++++++++++++++++++-- tasks/unit_tests.py | 5 ++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/tasks/custom_task/custom_task.py b/tasks/custom_task/custom_task.py index e004cdd2b43d3b..99e87cc2284502 100644 --- a/tasks/custom_task/custom_task.py +++ b/tasks/custom_task/custom_task.py @@ -15,6 +15,7 @@ from invoke import Context from tasks.libs.common.color import color_message +from tasks.libs.common.utils import running_in_ci DD_INVOKE_LOGS_FILE = "dd_invoke.log" WIN_TEMP_FOLDER = "C:\\Windows\\Temp" @@ -31,6 +32,26 @@ def get_dd_invoke_logs_path() -> str: return os.path.join(temp_folder, DD_INVOKE_LOGS_FILE) +def get_running_modes() -> list[str]: + """ + List the running modes of the task. + If the task is run via pre-commit -> "pre_commit" + If the task is run via unittest -> "invoke_unit_tests" + If the task is run in the ci -> "ci" + Neither pre-commit nor ci -> "manual" + """ + # This will catch when devs are running the unit tests with the unittest module directly. + # When running the unit tests with the invoke command, the INVOKE_UNIT_TESTS env variable is set. + is_running_ut = "unittest" in " ".join(sys.argv) + running_modes = { + "pre_commit": os.environ.get("PRE_COMMIT", 0) == "1", + "invoke_unit_tests": is_running_ut or os.environ.get("INVOKE_UNIT_TESTS", 0) == "1", + "ci": running_in_ci(), + } + running_modes["manual"] = not (running_modes["pre_commit"] or running_modes["ci"]) + return [mode for mode, is_running in running_modes.items() if is_running] + + def log_invoke_task( log_path: str, name: str, module: str, task_datetime: str, duration: float, task_result: str ) -> None: @@ -47,11 +68,11 @@ def log_invoke_task( """ logging.basicConfig(filename=log_path, level=logging.INFO, format='%(message)s') user = getuser() - running_mode = "pre_commit" if os.environ.get("PRE_COMMIT", 0) == "1" else "manual" + running_modes = get_running_modes() task_info = { "name": name, "module": module, - "running_mode": running_mode, + "running_modes": running_modes, "datetime": task_datetime, "duration": duration, "user": user, diff --git a/tasks/unit_tests.py b/tasks/unit_tests.py index 3fede59cbb9829..5b75686e30969d 100644 --- a/tasks/unit_tests.py +++ b/tasks/unit_tests.py @@ -8,4 +8,7 @@ def invoke_unit_tests(ctx): """ Run the unit tests on the invoke tasks """ - ctx.run(f"'{sys.executable}' -m unittest discover -s tasks -p '*_tests.py'", env={"GITLAB_TOKEN": "fake_token"}) + ctx.run( + f"'{sys.executable}' -m unittest discover -s tasks -p '*_tests.py'", + env={"GITLAB_TOKEN": "fake_token", "INVOKE_UNIT_TESTS": "1"}, + ) From d9ff65e15240ec2416d945a9df67ea33049ef73c Mon Sep 17 00:00:00 2001 From: Arthur Bellal Date: Fri, 24 May 2024 16:04:42 +0200 Subject: [PATCH 28/32] (fleet) install the agent oci by default when remote updates are enabled (#25898) --- pkg/fleet/installer/default_packages.go | 13 +++++++------ pkg/fleet/installer/default_packages_test.go | 6 ++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/fleet/installer/default_packages.go b/pkg/fleet/installer/default_packages.go index c1babb3dc0afbf..6ea16892b54bb7 100644 --- a/pkg/fleet/installer/default_packages.go +++ b/pkg/fleet/installer/default_packages.go @@ -13,10 +13,11 @@ import ( ) type defaultPackage struct { - name string - released bool - releasedBySite []string - condition func(*env.Env) bool + name string + released bool + releasedBySite []string + releasedWithRemoteUpdates bool + condition func(*env.Env) bool } var defaultPackagesList = []defaultPackage{ @@ -26,7 +27,7 @@ var defaultPackagesList = []defaultPackage{ {name: "datadog-apm-library-js", released: false, condition: apmInjectEnabled}, {name: "datadog-apm-library-dotnet", released: false, condition: apmInjectEnabled}, {name: "datadog-apm-library-python", released: false, condition: apmInjectEnabled}, - {name: "datadog-agent", released: false}, + {name: "datadog-agent", released: false, releasedWithRemoteUpdates: true}, } // DefaultPackages resolves the default packages URLs to install based on the environment. @@ -37,7 +38,7 @@ func DefaultPackages(env *env.Env) []string { func defaultPackages(env *env.Env, defaultPackages []defaultPackage) []string { var packages []string for _, p := range defaultPackages { - released := p.released || slices.Contains(p.releasedBySite, env.Site) + released := p.released || slices.Contains(p.releasedBySite, env.Site) || (p.releasedWithRemoteUpdates && env.RemoteUpdates) forcedInstall := env.DefaultPackagesInstallOverride[p.name] condition := p.condition == nil || p.condition(env) if (released && condition) || forcedInstall { diff --git a/pkg/fleet/installer/default_packages_test.go b/pkg/fleet/installer/default_packages_test.go index f1b96de7206bea..0ecb3602e97de7 100644 --- a/pkg/fleet/installer/default_packages_test.go +++ b/pkg/fleet/installer/default_packages_test.go @@ -64,6 +64,12 @@ func TestDefaultPackages(t *testing.T) { env: &env.Env{}, expected: []pkg{{n: "datadog-agent", v: "latest"}}, }, + { + name: "Package released with remote updates", + packages: []defaultPackage{{name: "datadog-agent", released: false, releasedWithRemoteUpdates: true}}, + env: &env.Env{RemoteUpdates: true}, + expected: []pkg{{n: "datadog-agent", v: "latest"}}, + }, { name: "Package released to another site", packages: []defaultPackage{{name: "datadog-agent", releasedBySite: []string{"datadoghq.eu"}}}, From 5e790668711cccec9d24e55046745571ce9fad1a Mon Sep 17 00:00:00 2001 From: Pierre Gimalac Date: Fri, 24 May 2024 17:50:53 +0200 Subject: [PATCH 29/32] [ASCII-1790] Send metrics with number of go dependencies for each build (#25854) * feat: send metrics with number of go dependencies for each build * fix: add CGO_ENABLED env * refactor: move constant metric names to globals --- .github/CODEOWNERS | 1 + .gitlab/JOBOWNERS | 4 +- .gitlab/source_test/golang_deps_diff.yml | 15 +++ tasks/__init__.py | 2 + tasks/diff.py | 108 ++++++++++---------- tasks/go_deps.py | 121 +++++++++++++++++++++++ 6 files changed, 195 insertions(+), 56 deletions(-) create mode 100644 tasks/go_deps.py diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b210596337390b..5eaecccef6f2ee 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -510,6 +510,7 @@ /tasks/ @DataDog/agent-developer-tools @DataDog/agent-ci-experience /tasks/msi.py @DataDog/windows-agent /tasks/agent.py @DataDog/agent-shared-components +/tasks/go_deps.py @DataDog/agent-shared-components /tasks/dogstatsd.py @DataDog/agent-metrics-logs /tasks/update_go.py @DataDog/agent-shared-components /tasks/unit-tests/update_go_tests.py @DataDog/agent-shared-components diff --git a/.gitlab/JOBOWNERS b/.gitlab/JOBOWNERS index a374fe5bcc948b..7b0e6564393b7e 100644 --- a/.gitlab/JOBOWNERS +++ b/.gitlab/JOBOWNERS @@ -13,8 +13,8 @@ security_go_generate_check @DataDog/agent-security prepare_sysprobe_ebpf_functional_tests* @DataDog/ebpf-platform prepare_secagent_ebpf_functional_tests* @DataDog/agent-security -# Golang dependency list generation (currently disabled) -golang_deps_generate @DataDog/agent-shared-components +# Send count metrics about Golang dependencies +golang_deps_send_count_metrics @DataDog/agent-shared-components # Golang dependency diff generation golang_deps_diff @DataDog/ebpf-platform golang_deps_commenter @DataDog/ebpf-platform diff --git a/.gitlab/source_test/golang_deps_diff.yml b/.gitlab/source_test/golang_deps_diff.yml index de25a073e73f7f..28d9c3f5fe4d50 100644 --- a/.gitlab/source_test/golang_deps_diff.yml +++ b/.gitlab/source_test/golang_deps_diff.yml @@ -47,3 +47,18 @@ golang_deps_commenter: exit 0 fi exit $exitcode + +golang_deps_send_count_metrics: + stage: source_test + image: 486234852809.dkr.ecr.us-east-1.amazonaws.com/ci/datadog-agent-buildimages/deb_x64$DATADOG_AGENT_BUILDIMAGES_SUFFIX:$DATADOG_AGENT_BUILDIMAGES + tags: ["arch:amd64"] + rules: + - when: on_success + needs: ["go_deps"] + before_script: + - source /root/.bashrc + - !reference [.retrieve_linux_go_deps] + script: + # Get API key to send metrics + - export DD_API_KEY=$($CI_PROJECT_DIR/tools/ci/aws_ssm_get_wrapper.sh $API_KEY_ORG2_SSM_NAME) + - inv -e go-deps.send-count-metrics --git-sha "${CI_COMMIT_SHA}" --git-ref "${CI_COMMIT_REF_NAME}" diff --git a/tasks/__init__.py b/tasks/__init__.py index f4f76250f0b680..7307bea2fd61a7 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -23,6 +23,7 @@ epforwarder, fakeintake, github_tasks, + go_deps, installer, kmt, linter, @@ -142,6 +143,7 @@ ns.add_collection(ebpf) ns.add_collection(emacs) ns.add_collection(epforwarder) +ns.add_collection(go_deps) ns.add_collection(linter) ns.add_collection(msi) ns.add_collection(github_tasks, "github") diff --git a/tasks/diff.py b/tasks/diff.py index 1b8a0574394b12..fa334ec2e90db8 100644 --- a/tasks/diff.py +++ b/tasks/diff.py @@ -15,6 +15,57 @@ from tasks.libs.common.utils import check_uncommitted_changes from tasks.release import _get_release_json_value +BINARIES = { + "agent": { + "entrypoint": "cmd/agent", + "platforms": ["linux/x64", "linux/arm64", "win32/x64", "darwin/x64", "darwin/arm64"], + }, + "iot-agent": { + "build": "agent", + "entrypoint": "cmd/agent", + "flavor": AgentFlavor.iot, + "platforms": ["linux/x64", "linux/arm64"], + }, + "heroku-agent": { + "build": "agent", + "entrypoint": "cmd/agent", + "flavor": AgentFlavor.heroku, + "platforms": ["linux/x64"], + }, + "cluster-agent": {"entrypoint": "cmd/cluster-agent", "platforms": ["linux/x64", "linux/arm64"]}, + "cluster-agent-cloudfoundry": { + "entrypoint": "cmd/cluster-agent-cloudfoundry", + "platforms": ["linux/x64", "linux/arm64"], + }, + "dogstatsd": {"entrypoint": "cmd/dogstatsd", "platforms": ["linux/x64", "linux/arm64"]}, + "process-agent": { + "entrypoint": "cmd/process-agent", + "platforms": ["linux/x64", "linux/arm64", "win32/x64", "darwin/x64", "darwin/arm64"], + }, + "heroku-process-agent": { + "build": "process-agent", + "entrypoint": "cmd/process-agent", + "flavor": AgentFlavor.heroku, + "platforms": ["linux/x64"], + }, + "security-agent": { + "entrypoint": "cmd/security-agent", + "platforms": ["linux/x64", "linux/arm64"], + }, + "serverless": {"entrypoint": "cmd/serverless", "platforms": ["linux/x64", "linux/arm64"]}, + "system-probe": {"entrypoint": "cmd/system-probe", "platforms": ["linux/x64", "linux/arm64", "win32/x64"]}, + "trace-agent": { + "entrypoint": "cmd/trace-agent", + "platforms": ["linux/x64", "linux/arm64", "win32/x64", "darwin/x64", "darwin/arm64"], + }, + "heroku-trace-agent": { + "build": "trace-agent", + "entrypoint": "cmd/trace-agent", + "flavor": AgentFlavor.heroku, + "platforms": ["linux/x64"], + }, +} + @task def go_deps(ctx, baseline_ref=None, report_file=None): @@ -35,57 +86,6 @@ def go_deps(ctx, baseline_ref=None, report_file=None): base_branch = _get_release_json_value("base_branch") baseline_ref = ctx.run(f"git merge-base {commit_sha} origin/{base_branch}", hide=True).stdout.strip() - # platforms are the agent task recognized os/platform and arch values, not Go-specific values - binaries = { - "agent": { - "entrypoint": "cmd/agent", - "platforms": ["linux/x64", "linux/arm64", "win32/x64", "darwin/x64", "darwin/arm64"], - }, - "iot-agent": { - "build": "agent", - "entrypoint": "cmd/agent", - "flavor": AgentFlavor.iot, - "platforms": ["linux/x64", "linux/arm64"], - }, - "heroku-agent": { - "build": "agent", - "entrypoint": "cmd/agent", - "flavor": AgentFlavor.heroku, - "platforms": ["linux/x64"], - }, - "cluster-agent": {"entrypoint": "cmd/cluster-agent", "platforms": ["linux/x64", "linux/arm64"]}, - "cluster-agent-cloudfoundry": { - "entrypoint": "cmd/cluster-agent-cloudfoundry", - "platforms": ["linux/x64", "linux/arm64"], - }, - "dogstatsd": {"entrypoint": "cmd/dogstatsd", "platforms": ["linux/x64", "linux/arm64"]}, - "process-agent": { - "entrypoint": "cmd/process-agent", - "platforms": ["linux/x64", "linux/arm64", "win32/x64", "darwin/x64", "darwin/arm64"], - }, - "heroku-process-agent": { - "build": "process-agent", - "entrypoint": "cmd/process-agent", - "flavor": AgentFlavor.heroku, - "platforms": ["linux/x64"], - }, - "security-agent": { - "entrypoint": "cmd/security-agent", - "platforms": ["linux/x64", "linux/arm64"], - }, - "serverless": {"entrypoint": "cmd/serverless", "platforms": ["linux/x64", "linux/arm64"]}, - "system-probe": {"entrypoint": "cmd/system-probe", "platforms": ["linux/x64", "linux/arm64", "win32/x64"]}, - "trace-agent": { - "entrypoint": "cmd/trace-agent", - "platforms": ["linux/x64", "linux/arm64", "win32/x64", "darwin/x64", "darwin/arm64"], - }, - "heroku-trace-agent": { - "build": "trace-agent", - "entrypoint": "cmd/trace-agent", - "flavor": AgentFlavor.heroku, - "platforms": ["linux/x64"], - }, - } diffs = {} dep_cmd = "go list -f '{{ range .Deps }}{{ printf \"%s\\n\" . }}{{end}}'" @@ -97,7 +97,7 @@ def go_deps(ctx, baseline_ref=None, report_file=None): if branch_ref: ctx.run(f"git checkout -q {branch_ref}") - for binary, details in binaries.items(): + for binary, details in BINARIES.items(): with ctx.cd(details.get("entrypoint")): for combo in details.get("platforms"): platform, arch = combo.split("/") @@ -114,7 +114,7 @@ def go_deps(ctx, baseline_ref=None, report_file=None): ctx.run(f"git checkout -q {current_branch}") # compute diffs for each target - for binary, details in binaries.items(): + for binary, details in BINARIES.items(): for combo in details.get("platforms"): platform, arch = combo.split("/") goos, goarch = GOOS_MAPPING.get(platform), GOARCH_MAPPING.get(arch) @@ -135,7 +135,7 @@ def go_deps(ctx, baseline_ref=None, report_file=None): f"Comparison: {commit_sha}\n", "
" + model.Syscall(s).String() + "
", ] - for binary, details in binaries.items(): + for binary, details in BINARIES.items(): for combo in details.get("platforms"): platform, arch = combo.split("/") goos, goarch = GOOS_MAPPING.get(platform), GOARCH_MAPPING.get(arch) diff --git a/tasks/go_deps.py b/tasks/go_deps.py new file mode 100644 index 00000000000000..b7bcbc25fd1ad8 --- /dev/null +++ b/tasks/go_deps.py @@ -0,0 +1,121 @@ +import datetime +import os +from collections.abc import Iterable + +from invoke.context import Context +from invoke.exceptions import Exit +from invoke.tasks import task + +from tasks.build_tags import get_default_build_tags +from tasks.diff import BINARIES +from tasks.flavor import AgentFlavor +from tasks.go import GOARCH_MAPPING, GOOS_MAPPING +from tasks.libs.common.color import color_message +from tasks.libs.common.datadog_api import create_gauge, send_metrics + +METRIC_GO_DEPS_ALL_NAME = "datadog.agent.go_dependencies.all" +METRIC_GO_DEPS_EXTERNAL_NAME = "datadog.agent.go_dependencies.external" + + +def compute_count_metric( + ctx: Context, + build: str, + flavor: AgentFlavor, + platform: str, + arch: str, + entrypoint: str, + timestamp: int | None = None, + extra_tags: Iterable[str] = (), +): + """ + Compute a metric representing the number of Go dependencies of the given build/flavor/platform/arch, + and one with only dependencies outside of the agent repository. + """ + + if not timestamp: + timestamp = int(datetime.datetime.now(datetime.UTC).timestamp()) + + goos, goarch = GOOS_MAPPING[platform], GOARCH_MAPPING[arch] + build_tags = get_default_build_tags(build=build, flavor=flavor, platform=platform) + + # need to explicitly enable CGO to also include CGO-only deps when checking different platforms + env = {"GOOS": goos, "GOARCH": goarch, "CGO_ENABLED": "1"} + cmd = "go list -f '{{ join .Deps \"\\n\"}}'" + with ctx.cd(entrypoint): + res = ctx.run( + f"{cmd} -tags {','.join(build_tags)}", + env=env, + hide='out', # don't hide errors + ) + assert res + + deps = res.stdout.strip().split("\n") + count = len(deps) + external = sum(1 for dep in deps if not dep.startswith("github.com/DataDog/datadog-agent/")) + + tags = [ + f"build:{build}", + f"flavor:{flavor.name}", + f"os:{goos}", + f"arch:{goarch}", + ] + tags.extend(extra_tags) + + metric_count = create_gauge(METRIC_GO_DEPS_ALL_NAME, timestamp, count, tags=tags) + metric_external = create_gauge(METRIC_GO_DEPS_EXTERNAL_NAME, timestamp, external, tags=tags) + return metric_count, metric_external + + +def compute_all_count_metrics(ctx: Context, extra_tags: Iterable[str] = ()): + """ + Compute metrics representing the number of Go dependencies of every build/flavor/platform/arch. + """ + + timestamp = int(datetime.datetime.now(datetime.UTC).timestamp()) + + series = [] + for binary, details in BINARIES.items(): + for combo in details["platforms"]: + platform, arch = combo.split("/") + flavor = details.get("flavor", AgentFlavor.base) + build = details.get("build", binary) + entrypoint = details["entrypoint"] + + metric_count, metric_external = compute_count_metric( + ctx, build, flavor, platform, arch, entrypoint, timestamp, extra_tags=extra_tags + ) + series.append(metric_count) + series.append(metric_external) + + return series + + +@task +def send_count_metrics( + ctx: Context, + git_sha: str, + git_ref: str | None = None, + send_series: bool = True, +): + if send_series and not os.environ.get("DD_API_KEY"): + raise Exit( + code=1, + message=color_message( + "DD_API_KEY environment variable not set, cannot send pipeline metrics to the backend", "red" + ), + ) + + extra_tags = [ + f"git_sha:{git_sha}", + ] + if git_ref: + extra_tags.append(f"git_ref:{git_ref}") + + series = compute_all_count_metrics(ctx, extra_tags=extra_tags) + print(color_message("Data collected:", "blue")) + print(series) + + if send_series: + print(color_message("Sending metrics to Datadog", "blue")) + send_metrics(series=series) + print(color_message("Done", "green")) From 5b9f9aa80608ef9294da08b219bd5b0ccbd3df67 Mon Sep 17 00:00:00 2001 From: Nicolas Schweitzer Date: Fri, 24 May 2024 18:17:46 +0200 Subject: [PATCH 30/32] fix(tools): Set the correct jira project name for telemetry and analytics team (#25907) --- tasks/libs/pipeline/github_jira_map.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/libs/pipeline/github_jira_map.yaml b/tasks/libs/pipeline/github_jira_map.yaml index ad369e5ad57cc6..81438fb65b4de6 100644 --- a/tasks/libs/pipeline/github_jira_map.yaml +++ b/tasks/libs/pipeline/github_jira_map.yaml @@ -32,7 +32,7 @@ '@datadog/debugger': DEBUG '@datadog/database-monitoring': DBMON '@datadog/agent-cspm': SEC -'@datadog/telemetry-and-analytics': DEFAULT_JIRA_PROJECT +'@datadog/telemetry-and-analytics': AIT '@datadog/asm-go': APPSEC '@datadog/agent-build-and-releases': BARX '@datadog/agent-developer-tools': ADXT From 2beb26aded7274a40a25aa764365881293225819 Mon Sep 17 00:00:00 2001 From: Pierre Gimalac Date: Fri, 24 May 2024 18:17:50 +0200 Subject: [PATCH 31/32] [ASCII-1613] Split `dogstatsd/replay` into new component hierarchy (#25512) * refactor: move component and exported symbols to def folder * refactor: move Mock interface and module to their own directories * refactor: move fx related functions in fx directory * refactor: move impl to its own directory * refactor: remove CapPool global variable * fix: review comments about mock * fix: review comments about package naming * chore: rename replaydef imports to replay * chore: update serverless dependency list --- .../subcommands/dogstatsdreplay/command.go | 2 +- cmd/agent/subcommands/jmx/command.go | 2 +- cmd/agent/subcommands/run/command.go | 2 +- cmd/agent/subcommands/run/command_windows.go | 2 +- cmd/serverless/linux_dependencies_amd64.txt | 3 +- cmd/serverless/linux_dependencies_arm64.txt | 3 +- comp/README.md | 2 +- comp/api/api/apiimpl/api.go | 2 +- comp/api/api/apiimpl/api_test.go | 5 +- comp/api/api/apiimpl/grpc.go | 2 +- comp/api/api/apiimpl/server.go | 2 +- comp/api/api/apiimpl/server_cmd.go | 2 +- comp/dogstatsd/bundle.go | 4 +- comp/dogstatsd/bundle_mock.go | 4 +- .../listeners/named_pipe_nowindows.go | 2 +- .../dogstatsd/listeners/named_pipe_windows.go | 2 +- comp/dogstatsd/listeners/udp.go | 2 +- comp/dogstatsd/listeners/uds_common.go | 4 +- comp/dogstatsd/listeners/uds_datagram.go | 2 +- comp/dogstatsd/listeners/uds_linux.go | 2 +- comp/dogstatsd/listeners/uds_stream.go | 2 +- comp/dogstatsd/replay/{ => def}/component.go | 43 ++++++++++-------- comp/dogstatsd/replay/fx-mock/module.go | 25 ++++++++++ comp/dogstatsd/replay/fx/module.go | 23 ++++++++++ comp/dogstatsd/replay/{ => impl}/capture.go | 31 ++++++------- comp/dogstatsd/replay/{ => impl}/file.go | 0 .../replay/{ => impl}/file_common.go | 0 .../replay/{ => impl}/file_serverless.go | 0 comp/dogstatsd/replay/{ => impl}/file_test.go | 0 comp/dogstatsd/replay/{ => impl}/reader.go | 0 .../replay/{ => impl}/reader_creator.go | 0 .../{ => impl}/reader_creator_serverless.go | 0 .../dogstatsd/replay/{ => impl}/reader_nix.go | 0 .../replay/{ => impl}/reader_test.go | 0 .../replay/{ => impl}/reader_windows.go | 0 .../resources/test/datadog-capture.dog | Bin .../resources/test/datadog-capture.dog.zstd | Bin .../dogstatsd/replay/{ => impl}/util_linux.go | 4 +- .../replay/{ => impl}/util_nolinux.go | 0 comp/dogstatsd/replay/{ => impl}/writer.go | 41 +++-------------- .../replay/{ => impl}/writer_test.go | 3 +- comp/dogstatsd/replay/{ => mock}/mock.go | 34 ++++++++------ comp/dogstatsd/server/server.go | 2 +- comp/dogstatsd/server/server_test.go | 7 +-- comp/dogstatsd/server/serverless.go | 2 +- pkg/cli/subcommands/check/command.go | 2 +- 46 files changed, 153 insertions(+), 117 deletions(-) rename comp/dogstatsd/replay/{ => def}/component.go (58%) create mode 100644 comp/dogstatsd/replay/fx-mock/module.go create mode 100644 comp/dogstatsd/replay/fx/module.go rename comp/dogstatsd/replay/{ => impl}/capture.go (83%) rename comp/dogstatsd/replay/{ => impl}/file.go (100%) rename comp/dogstatsd/replay/{ => impl}/file_common.go (100%) rename comp/dogstatsd/replay/{ => impl}/file_serverless.go (100%) rename comp/dogstatsd/replay/{ => impl}/file_test.go (100%) rename comp/dogstatsd/replay/{ => impl}/reader.go (100%) rename comp/dogstatsd/replay/{ => impl}/reader_creator.go (100%) rename comp/dogstatsd/replay/{ => impl}/reader_creator_serverless.go (100%) rename comp/dogstatsd/replay/{ => impl}/reader_nix.go (100%) rename comp/dogstatsd/replay/{ => impl}/reader_test.go (100%) rename comp/dogstatsd/replay/{ => impl}/reader_windows.go (100%) rename comp/dogstatsd/replay/{ => impl}/resources/test/datadog-capture.dog (100%) rename comp/dogstatsd/replay/{ => impl}/resources/test/datadog-capture.dog.zstd (100%) rename comp/dogstatsd/replay/{ => impl}/util_linux.go (85%) rename comp/dogstatsd/replay/{ => impl}/util_nolinux.go (100%) rename comp/dogstatsd/replay/{ => impl}/writer.go (89%) rename comp/dogstatsd/replay/{ => impl}/writer_test.go (97%) rename comp/dogstatsd/replay/{ => mock}/mock.go (73%) diff --git a/cmd/agent/subcommands/dogstatsdreplay/command.go b/cmd/agent/subcommands/dogstatsdreplay/command.go index 944440854459e5..a2892cbdb544d6 100644 --- a/cmd/agent/subcommands/dogstatsdreplay/command.go +++ b/cmd/agent/subcommands/dogstatsdreplay/command.go @@ -24,7 +24,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core" "github.com/DataDog/datadog-agent/comp/core/config" "github.com/DataDog/datadog-agent/comp/core/log" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/impl" "github.com/DataDog/datadog-agent/pkg/api/security" pkgconfig "github.com/DataDog/datadog-agent/pkg/config" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" diff --git a/cmd/agent/subcommands/jmx/command.go b/cmd/agent/subcommands/jmx/command.go index 1964e91602f88d..2f777d0b65a0e1 100644 --- a/cmd/agent/subcommands/jmx/command.go +++ b/cmd/agent/subcommands/jmx/command.go @@ -51,7 +51,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" serverdebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" "github.com/DataDog/datadog-agent/comp/forwarder/eventplatformreceiver" diff --git a/cmd/agent/subcommands/run/command.go b/cmd/agent/subcommands/run/command.go index eba899b5d8b4ff..f280e6125bb259 100644 --- a/cmd/agent/subcommands/run/command.go +++ b/cmd/agent/subcommands/run/command.go @@ -79,7 +79,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors" "github.com/DataDog/datadog-agent/comp/core/workloadmeta/defaults" "github.com/DataDog/datadog-agent/comp/dogstatsd" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" dogstatsddebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" dogstatsdStatusimpl "github.com/DataDog/datadog-agent/comp/dogstatsd/status/statusimpl" diff --git a/cmd/agent/subcommands/run/command_windows.go b/cmd/agent/subcommands/run/command_windows.go index 3b952426a371d7..a19d06869463dd 100644 --- a/cmd/agent/subcommands/run/command_windows.go +++ b/cmd/agent/subcommands/run/command_windows.go @@ -57,7 +57,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/tagger" "github.com/DataDog/datadog-agent/comp/core/telemetry" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" "github.com/DataDog/datadog-agent/comp/forwarder/defaultforwarder" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" diff --git a/cmd/serverless/linux_dependencies_amd64.txt b/cmd/serverless/linux_dependencies_amd64.txt index c0588d00a21453..6002ca82f2b1ea 100644 --- a/cmd/serverless/linux_dependencies_amd64.txt +++ b/cmd/serverless/linux_dependencies_amd64.txt @@ -93,7 +93,8 @@ github.com/DataDog/datadog-agent/comp/dogstatsd/mapper github.com/DataDog/datadog-agent/comp/dogstatsd/packets github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap/pidmapimpl -github.com/DataDog/datadog-agent/comp/dogstatsd/replay +github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def +github.com/DataDog/datadog-agent/comp/dogstatsd/replay/impl github.com/DataDog/datadog-agent/comp/dogstatsd/server github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug/serverdebugimpl diff --git a/cmd/serverless/linux_dependencies_arm64.txt b/cmd/serverless/linux_dependencies_arm64.txt index 6160db8295d3bc..9e1eb1c3aff51d 100644 --- a/cmd/serverless/linux_dependencies_arm64.txt +++ b/cmd/serverless/linux_dependencies_arm64.txt @@ -93,7 +93,8 @@ github.com/DataDog/datadog-agent/comp/dogstatsd/mapper github.com/DataDog/datadog-agent/comp/dogstatsd/packets github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap/pidmapimpl -github.com/DataDog/datadog-agent/comp/dogstatsd/replay +github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def +github.com/DataDog/datadog-agent/comp/dogstatsd/replay/impl github.com/DataDog/datadog-agent/comp/dogstatsd/server github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug/serverdebugimpl diff --git a/comp/README.md b/comp/README.md index a425a86aa8266a..f1a2a767bdcb75 100644 --- a/comp/README.md +++ b/comp/README.md @@ -204,7 +204,7 @@ Package pidmap implements a component for tracking pid and containerID relations ### [comp/dogstatsd/replay](https://pkg.go.dev/github.com/DataDog/datadog-agent/comp/dogstatsd/replay) -Package server implements a component to run the dogstatsd capture/replay +Package replay is a component to run the dogstatsd capture/replay ### [comp/dogstatsd/server](https://pkg.go.dev/github.com/DataDog/datadog-agent/comp/dogstatsd/server) diff --git a/comp/api/api/apiimpl/api.go b/comp/api/api/apiimpl/api.go index 4cd2ef704b59eb..e5b487cd5faa8a 100644 --- a/comp/api/api/apiimpl/api.go +++ b/comp/api/api/apiimpl/api.go @@ -21,7 +21,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/tagger" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" "github.com/DataDog/datadog-agent/comp/metadata/packagesigning" diff --git a/comp/api/api/apiimpl/api_test.go b/comp/api/api/apiimpl/api_test.go index aec84e79ee978e..4a70a0f93e5fcd 100644 --- a/comp/api/api/apiimpl/api_test.go +++ b/comp/api/api/apiimpl/api_test.go @@ -28,7 +28,8 @@ import ( "github.com/DataDog/datadog-agent/comp/core/tagger" "github.com/DataDog/datadog-agent/comp/core/tagger/taggerimpl" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" + replaymock "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/fx-mock" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" dogstatsddebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug/serverdebugimpl" @@ -87,7 +88,7 @@ func getComponentDependencies(t *testing.T) testdeps { hostnameimpl.MockModule(), flareimpl.MockModule(), dogstatsdServer.MockModule(), - replay.MockModule(), + replaymock.MockModule(), serverdebugimpl.MockModule(), hostimpl.MockModule(), inventoryagentimpl.MockModule(), diff --git a/comp/api/api/apiimpl/grpc.go b/comp/api/api/apiimpl/grpc.go index 5531c4b3106cac..1f5e42a2f20fbc 100644 --- a/comp/api/api/apiimpl/grpc.go +++ b/comp/api/api/apiimpl/grpc.go @@ -25,7 +25,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/tagger/taggerimpl/replay" taggerserver "github.com/DataDog/datadog-agent/comp/core/tagger/taggerimpl/server" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - dsdReplay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + dsdReplay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" "github.com/DataDog/datadog-agent/pkg/util/grpc" diff --git a/comp/api/api/apiimpl/server.go b/comp/api/api/apiimpl/server.go index 59ecf051dad52a..9b58b8a24f0ae4 100644 --- a/comp/api/api/apiimpl/server.go +++ b/comp/api/api/apiimpl/server.go @@ -23,7 +23,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/tagger" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" "github.com/DataDog/datadog-agent/comp/remote-config/rcservice" diff --git a/comp/api/api/apiimpl/server_cmd.go b/comp/api/api/apiimpl/server_cmd.go index 2f035e689f637a..eaa128411ebfb4 100644 --- a/comp/api/api/apiimpl/server_cmd.go +++ b/comp/api/api/apiimpl/server_cmd.go @@ -34,7 +34,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta" workloadmetaServer "github.com/DataDog/datadog-agent/comp/core/workloadmeta/server" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" dogstatsdServer "github.com/DataDog/datadog-agent/comp/dogstatsd/server" logsAgent "github.com/DataDog/datadog-agent/comp/logs/agent" "github.com/DataDog/datadog-agent/comp/remote-config/rcservice" diff --git a/comp/dogstatsd/bundle.go b/comp/dogstatsd/bundle.go index 58139c50f1f6cd..fb9eb5bdb95186 100644 --- a/comp/dogstatsd/bundle.go +++ b/comp/dogstatsd/bundle.go @@ -7,7 +7,7 @@ package dogstatsd //nolint:revive // TODO(AML) Fix revive linter import ( "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap/pidmapimpl" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replayfx "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/fx" "github.com/DataDog/datadog-agent/comp/dogstatsd/server" "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug/serverdebugimpl" "github.com/DataDog/datadog-agent/comp/dogstatsd/statsd" @@ -20,7 +20,7 @@ import ( func Bundle() fxutil.BundleOptions { return fxutil.Bundle( serverdebugimpl.Module(), - replay.Module(), + replayfx.Module(), pidmapimpl.Module(), server.Module()) } diff --git a/comp/dogstatsd/bundle_mock.go b/comp/dogstatsd/bundle_mock.go index 737d9b2fc978fc..018bfe2465572d 100644 --- a/comp/dogstatsd/bundle_mock.go +++ b/comp/dogstatsd/bundle_mock.go @@ -9,7 +9,7 @@ package dogstatsd import ( "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap/pidmapimpl" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replayfx "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/fx" "github.com/DataDog/datadog-agent/comp/dogstatsd/server" "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug/serverdebugimpl" "github.com/DataDog/datadog-agent/comp/dogstatsd/statsd" @@ -21,7 +21,7 @@ func MockBundle() fxutil.BundleOptions { return fxutil.Bundle( serverdebugimpl.MockModule(), server.MockModule(), - replay.Module(), + replayfx.Module(), pidmapimpl.Module()) } diff --git a/comp/dogstatsd/listeners/named_pipe_nowindows.go b/comp/dogstatsd/listeners/named_pipe_nowindows.go index b0e3ae7c747bd6..40478770f02020 100644 --- a/comp/dogstatsd/listeners/named_pipe_nowindows.go +++ b/comp/dogstatsd/listeners/named_pipe_nowindows.go @@ -10,7 +10,7 @@ import ( "errors" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/pkg/config" ) diff --git a/comp/dogstatsd/listeners/named_pipe_windows.go b/comp/dogstatsd/listeners/named_pipe_windows.go index 8208bf3ea564b0..22d0bcd9534066 100644 --- a/comp/dogstatsd/listeners/named_pipe_windows.go +++ b/comp/dogstatsd/listeners/named_pipe_windows.go @@ -16,7 +16,7 @@ import ( "go.uber.org/atomic" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/util/log" diff --git a/comp/dogstatsd/listeners/udp.go b/comp/dogstatsd/listeners/udp.go index 3ccb3ecc33a270..bd6b146b0b705c 100644 --- a/comp/dogstatsd/listeners/udp.go +++ b/comp/dogstatsd/listeners/udp.go @@ -14,7 +14,7 @@ import ( "time" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/util/log" ) diff --git a/comp/dogstatsd/listeners/uds_common.go b/comp/dogstatsd/listeners/uds_common.go index 51e3f57f41232b..e36701337af32e 100644 --- a/comp/dogstatsd/listeners/uds_common.go +++ b/comp/dogstatsd/listeners/uds_common.go @@ -22,7 +22,7 @@ import ( "github.com/DataDog/datadog-agent/comp/dogstatsd/listeners/ratelimit" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/util/log" "github.com/DataDog/datadog-agent/pkg/util/optional" @@ -225,7 +225,7 @@ func (l *UDSListener) handleConnection(conn *net.UnixConn, closeFunc CloseFuncti var capBuff *replay.CaptureBuffer if l.trafficCapture != nil && l.trafficCapture.IsOngoing() { - capBuff = replay.CapPool.Get().(*replay.CaptureBuffer) + capBuff = new(replay.CaptureBuffer) capBuff.Pb.Ancillary = nil capBuff.Pb.Payload = nil capBuff.Pb.Pid = 0 diff --git a/comp/dogstatsd/listeners/uds_datagram.go b/comp/dogstatsd/listeners/uds_datagram.go index 2be4902cc9c399..fb5727de33265c 100644 --- a/comp/dogstatsd/listeners/uds_datagram.go +++ b/comp/dogstatsd/listeners/uds_datagram.go @@ -12,7 +12,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/util/log" "github.com/DataDog/datadog-agent/pkg/util/optional" diff --git a/comp/dogstatsd/listeners/uds_linux.go b/comp/dogstatsd/listeners/uds_linux.go index 8501b65b36f5fd..5b33d68fdc26e2 100644 --- a/comp/dogstatsd/listeners/uds_linux.go +++ b/comp/dogstatsd/listeners/uds_linux.go @@ -17,7 +17,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/pkg/util/cache" "github.com/DataDog/datadog-agent/pkg/util/containers" "github.com/DataDog/datadog-agent/pkg/util/containers/metrics/provider" diff --git a/comp/dogstatsd/listeners/uds_stream.go b/comp/dogstatsd/listeners/uds_stream.go index 1b953e846cb84b..35dc294954e76e 100644 --- a/comp/dogstatsd/listeners/uds_stream.go +++ b/comp/dogstatsd/listeners/uds_stream.go @@ -14,7 +14,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/pkg/config" "github.com/DataDog/datadog-agent/pkg/util/log" "github.com/DataDog/datadog-agent/pkg/util/optional" diff --git a/comp/dogstatsd/replay/component.go b/comp/dogstatsd/replay/def/component.go similarity index 58% rename from comp/dogstatsd/replay/component.go rename to comp/dogstatsd/replay/def/component.go index b15011724b22ca..824e2f2645d642 100644 --- a/comp/dogstatsd/replay/component.go +++ b/comp/dogstatsd/replay/def/component.go @@ -3,18 +3,13 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-present Datadog, Inc. -// Package server implements a component to run the dogstatsd capture/replay -// -//nolint:revive // TODO(AML) Fix revive linter +// Package replay is a component to run the dogstatsd capture/replay package replay import ( "time" - "go.uber.org/fx" - "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" - "github.com/DataDog/datadog-agent/pkg/util/fxutil" ) // team: agent-metrics-logs @@ -46,19 +41,31 @@ type Component interface { GetStartUpError() error } -// Mock implements mock-specific methods. -type Mock interface { - Component +// UnixDogstatsdMsg mirrors the exported fields of pkg/proto/pbgo/core/model.pb.go 'UnixDogstatsdMsg +// to avoid forcing the import of pbgo on every user of dogstatsd. +type UnixDogstatsdMsg struct { + Timestamp int64 + PayloadSize int32 + Payload []byte + Pid int32 + AncillarySize int32 + Ancillary []byte } -// Module defines the fx options for this component. -func Module() fxutil.Module { - return fxutil.Component( - fx.Provide(newTrafficCapture)) +// CaptureBuffer holds pointers to captured packet's buffers (and oob buffer if required) and the protobuf +// message used for serialization. +type CaptureBuffer struct { + Pb UnixDogstatsdMsg + Oob *[]byte + Pid int32 + ContainerID string + Buff *packets.Packet } -// MockModule defines the fx options for the mock component. -func MockModule() fxutil.Module { - return fxutil.Component( - fx.Provide(newMockTrafficCapture)) -} +const ( + // GUID will be used as the GUID during capture replays + // This is a magic number chosen for no particular reason other than the fact its + // quite large an improbable to match an actual Group ID on any given box. We + // need this number to identify replayed Unix socket ancillary credentials. + GUID = 999888777 +) diff --git a/comp/dogstatsd/replay/fx-mock/module.go b/comp/dogstatsd/replay/fx-mock/module.go new file mode 100644 index 00000000000000..dd7ac22fa7c21d --- /dev/null +++ b/comp/dogstatsd/replay/fx-mock/module.go @@ -0,0 +1,25 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//go:build test + +// Package server implements a component to run the dogstatsd capture/replay +// +//nolint:revive // TODO(AML) Fix revive linter +package fxmock + +import ( + replaymock "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/mock" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" +) + +// MockModule defines the fx options for the mock component. +func MockModule() fxutil.Module { + return fxutil.Component( + fxutil.ProvideComponentConstructor( + replaymock.NewTrafficCapture, + ), + ) +} diff --git a/comp/dogstatsd/replay/fx/module.go b/comp/dogstatsd/replay/fx/module.go new file mode 100644 index 00000000000000..933fed12e08419 --- /dev/null +++ b/comp/dogstatsd/replay/fx/module.go @@ -0,0 +1,23 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016-present Datadog, Inc. + +//nolint:revive // TODO(AML) Fix revive linter +package fx + +import ( + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/impl" + "github.com/DataDog/datadog-agent/pkg/util/fxutil" +) + +// team: agent-metrics-logs + +// Module defines the fx options for this component. +func Module() fxutil.Module { + return fxutil.Component( + fxutil.ProvideComponentConstructor( + replay.NewTrafficCapture, + ), + ) +} diff --git a/comp/dogstatsd/replay/capture.go b/comp/dogstatsd/replay/impl/capture.go similarity index 83% rename from comp/dogstatsd/replay/capture.go rename to comp/dogstatsd/replay/impl/capture.go index 6907e81e5e48f9..3e270d094a0b24 100644 --- a/comp/dogstatsd/replay/capture.go +++ b/comp/dogstatsd/replay/impl/capture.go @@ -3,6 +3,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-2021 Datadog, Inc. +//nolint:revive // TODO(AML) Fix revive linter package replay import ( @@ -13,29 +14,21 @@ import ( "time" "github.com/spf13/afero" - "go.uber.org/fx" configComponent "github.com/DataDog/datadog-agent/comp/core/config" + compdef "github.com/DataDog/datadog-agent/comp/def" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/pkg/config" ) -const ( - // GUID will be used as the GUID during capture replays - // This is a magic number chosen for no particular reason other than the fact its - // quite large an improbable to match an actual Group ID on any given box. We - // need this number to identify replayed Unix socket ancillary credentials. - GUID = 999888777 -) - -type dependencies struct { - fx.In - - Lc fx.Lifecycle +//nolint:revive // TODO(AML) Fix revive linter +type Requires struct { + Lc compdef.Lifecycle Config configComponent.Component } -// TrafficCapture allows capturing traffic from our listeners and writing it to file +// trafficCapture allows capturing traffic from our listeners and writing it to file type trafficCapture struct { writer *TrafficCaptureWriter config config.Reader @@ -47,16 +40,18 @@ type trafficCapture struct { // TODO: (components) - remove once serverless is an FX app // //nolint:revive // TODO(AML) Fix revive linter -func NewServerlessTrafficCapture() Component { +func NewServerlessTrafficCapture() replay.Component { tc := newTrafficCaptureCompat(config.Datadog) _ = tc.configure(context.TODO()) return tc } // TODO: (components) - merge with newTrafficCaptureCompat once NewServerlessTrafficCapture is removed -func newTrafficCapture(deps dependencies) Component { +// +//nolint:revive // TODO(AML) Fix revive linter +func NewTrafficCapture(deps Requires) replay.Component { tc := newTrafficCaptureCompat(deps.Config) - deps.Lc.Append(fx.Hook{ + deps.Lc.Append(compdef.Hook{ OnStart: tc.configure, }) @@ -134,7 +129,7 @@ func (tc *trafficCapture) RegisterOOBPoolManager(p *packets.PoolManager) error { } // Enqueue enqueues a capture buffer so it's written to file. -func (tc *trafficCapture) Enqueue(msg *CaptureBuffer) bool { +func (tc *trafficCapture) Enqueue(msg *replay.CaptureBuffer) bool { tc.RLock() defer tc.RUnlock() return tc.writer.Enqueue(msg) diff --git a/comp/dogstatsd/replay/file.go b/comp/dogstatsd/replay/impl/file.go similarity index 100% rename from comp/dogstatsd/replay/file.go rename to comp/dogstatsd/replay/impl/file.go diff --git a/comp/dogstatsd/replay/file_common.go b/comp/dogstatsd/replay/impl/file_common.go similarity index 100% rename from comp/dogstatsd/replay/file_common.go rename to comp/dogstatsd/replay/impl/file_common.go diff --git a/comp/dogstatsd/replay/file_serverless.go b/comp/dogstatsd/replay/impl/file_serverless.go similarity index 100% rename from comp/dogstatsd/replay/file_serverless.go rename to comp/dogstatsd/replay/impl/file_serverless.go diff --git a/comp/dogstatsd/replay/file_test.go b/comp/dogstatsd/replay/impl/file_test.go similarity index 100% rename from comp/dogstatsd/replay/file_test.go rename to comp/dogstatsd/replay/impl/file_test.go diff --git a/comp/dogstatsd/replay/reader.go b/comp/dogstatsd/replay/impl/reader.go similarity index 100% rename from comp/dogstatsd/replay/reader.go rename to comp/dogstatsd/replay/impl/reader.go diff --git a/comp/dogstatsd/replay/reader_creator.go b/comp/dogstatsd/replay/impl/reader_creator.go similarity index 100% rename from comp/dogstatsd/replay/reader_creator.go rename to comp/dogstatsd/replay/impl/reader_creator.go diff --git a/comp/dogstatsd/replay/reader_creator_serverless.go b/comp/dogstatsd/replay/impl/reader_creator_serverless.go similarity index 100% rename from comp/dogstatsd/replay/reader_creator_serverless.go rename to comp/dogstatsd/replay/impl/reader_creator_serverless.go diff --git a/comp/dogstatsd/replay/reader_nix.go b/comp/dogstatsd/replay/impl/reader_nix.go similarity index 100% rename from comp/dogstatsd/replay/reader_nix.go rename to comp/dogstatsd/replay/impl/reader_nix.go diff --git a/comp/dogstatsd/replay/reader_test.go b/comp/dogstatsd/replay/impl/reader_test.go similarity index 100% rename from comp/dogstatsd/replay/reader_test.go rename to comp/dogstatsd/replay/impl/reader_test.go diff --git a/comp/dogstatsd/replay/reader_windows.go b/comp/dogstatsd/replay/impl/reader_windows.go similarity index 100% rename from comp/dogstatsd/replay/reader_windows.go rename to comp/dogstatsd/replay/impl/reader_windows.go diff --git a/comp/dogstatsd/replay/resources/test/datadog-capture.dog b/comp/dogstatsd/replay/impl/resources/test/datadog-capture.dog similarity index 100% rename from comp/dogstatsd/replay/resources/test/datadog-capture.dog rename to comp/dogstatsd/replay/impl/resources/test/datadog-capture.dog diff --git a/comp/dogstatsd/replay/resources/test/datadog-capture.dog.zstd b/comp/dogstatsd/replay/impl/resources/test/datadog-capture.dog.zstd similarity index 100% rename from comp/dogstatsd/replay/resources/test/datadog-capture.dog.zstd rename to comp/dogstatsd/replay/impl/resources/test/datadog-capture.dog.zstd diff --git a/comp/dogstatsd/replay/util_linux.go b/comp/dogstatsd/replay/impl/util_linux.go similarity index 85% rename from comp/dogstatsd/replay/util_linux.go rename to comp/dogstatsd/replay/impl/util_linux.go index 94560e8d2c3c92..2300da2bcd9e93 100644 --- a/comp/dogstatsd/replay/util_linux.go +++ b/comp/dogstatsd/replay/impl/util_linux.go @@ -10,6 +10,8 @@ package replay import ( "os" "syscall" + + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" ) // GetUcredsForPid returns the replay ucreds for the specified pid @@ -17,7 +19,7 @@ func GetUcredsForPid(pid int32) []byte { ucreds := &syscall.Ucred{ Pid: int32(os.Getpid()), Uid: uint32(pid), - Gid: GUID, + Gid: replay.GUID, } return syscall.UnixCredentials(ucreds) diff --git a/comp/dogstatsd/replay/util_nolinux.go b/comp/dogstatsd/replay/impl/util_nolinux.go similarity index 100% rename from comp/dogstatsd/replay/util_nolinux.go rename to comp/dogstatsd/replay/impl/util_nolinux.go diff --git a/comp/dogstatsd/replay/writer.go b/comp/dogstatsd/replay/impl/writer.go similarity index 89% rename from comp/dogstatsd/replay/writer.go rename to comp/dogstatsd/replay/impl/writer.go index d4d83637f4d907..82c7146d08de49 100644 --- a/comp/dogstatsd/replay/writer.go +++ b/comp/dogstatsd/replay/impl/writer.go @@ -23,6 +23,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/tagger" taggerproto "github.com/DataDog/datadog-agent/comp/core/tagger/proto" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" "github.com/DataDog/datadog-agent/pkg/util/log" "github.com/golang/protobuf/proto" @@ -32,27 +33,6 @@ const ( fileTemplate = "datadog-capture-%d" ) -// UnixDogstatsdMsg mirrors the exported fields of pkg/proto/pbgo/core/model.pb.go 'UnixDogstatsdMsg -// to avoid forcing the import of pbgo on every user of dogstatsd. -type UnixDogstatsdMsg struct { - Timestamp int64 - PayloadSize int32 - Payload []byte - Pid int32 - AncillarySize int32 - Ancillary []byte -} - -// CaptureBuffer holds pointers to captured packet's buffers (and oob buffer if required) and the protobuf -// message used for serialization. -type CaptureBuffer struct { - Pb UnixDogstatsdMsg - Oob *[]byte - Pid int32 - ContainerID string - Buff *packets.Packet -} - // for testing purposes // //nolint:unused @@ -69,18 +49,11 @@ var captureFs = backendFs{ fs: afero.NewOsFs(), } -// CapPool is a pool of CaptureBuffer -var CapPool = sync.Pool{ - New: func() interface{} { - return new(CaptureBuffer) - }, -} - // TrafficCaptureWriter allows writing dogstatsd traffic to a file. type TrafficCaptureWriter struct { zWriter *zstd.Writer writer *bufio.Writer - Traffic chan *CaptureBuffer + Traffic chan *replay.CaptureBuffer ongoing bool accepting bool @@ -97,14 +70,14 @@ type TrafficCaptureWriter struct { func NewTrafficCaptureWriter(depth int) *TrafficCaptureWriter { return &TrafficCaptureWriter{ - Traffic: make(chan *CaptureBuffer, depth), + Traffic: make(chan *replay.CaptureBuffer, depth), taggerState: make(map[int32]string), } } // processMessage receives a capture buffer and writes it to disk while also tracking // the PID map to be persisted to the taggerState. Should not normally be called directly. -func (tc *TrafficCaptureWriter) processMessage(msg *CaptureBuffer) error { +func (tc *TrafficCaptureWriter) processMessage(msg *replay.CaptureBuffer) error { err := tc.writeNext(msg) if err != nil { @@ -282,7 +255,7 @@ func (tc *TrafficCaptureWriter) StopCapture() { } // Enqueue enqueues a capture buffer so it's written to file. -func (tc *TrafficCaptureWriter) Enqueue(msg *CaptureBuffer) bool { +func (tc *TrafficCaptureWriter) Enqueue(msg *replay.CaptureBuffer) bool { tc.RLock() defer tc.RUnlock() @@ -389,9 +362,9 @@ func (tc *TrafficCaptureWriter) writeState() (int, error) { return n + 8, err } -// writeNext writes the next CaptureBuffer after serializing it to a protobuf format. +// writeNext writes the next replay.CaptureBuffer after serializing it to a protobuf format. // Continuing writes after an error calling this function would result in a corrupted file -func (tc *TrafficCaptureWriter) writeNext(msg *CaptureBuffer) error { +func (tc *TrafficCaptureWriter) writeNext(msg *replay.CaptureBuffer) error { pb := pb.UnixDogstatsdMsg{ Timestamp: msg.Pb.Timestamp, PayloadSize: msg.Pb.PayloadSize, diff --git a/comp/dogstatsd/replay/writer_test.go b/comp/dogstatsd/replay/impl/writer_test.go similarity index 97% rename from comp/dogstatsd/replay/writer_test.go rename to comp/dogstatsd/replay/impl/writer_test.go index ad6d99d27264bb..054ea34d2f9433 100644 --- a/comp/dogstatsd/replay/writer_test.go +++ b/comp/dogstatsd/replay/impl/writer_test.go @@ -21,6 +21,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/config" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" pb "github.com/DataDog/datadog-agent/pkg/proto/pbgo/core" "github.com/DataDog/datadog-agent/pkg/util/fxutil" ) @@ -65,7 +66,7 @@ func writerTest(t *testing.T, z bool) { time.Sleep(duration) for i := 0; i < iterations; i++ { - buff := CapPool.Get().(*CaptureBuffer) + buff := new(replay.CaptureBuffer) pkt := manager.Get().(*packets.Packet) pkt.Buffer = []byte("foo.bar|5|#some:tag") pkt.Source = packets.UDS diff --git a/comp/dogstatsd/replay/mock.go b/comp/dogstatsd/replay/mock/mock.go similarity index 73% rename from comp/dogstatsd/replay/mock.go rename to comp/dogstatsd/replay/mock/mock.go index dd41179f33a5c5..787166f0a1761f 100644 --- a/comp/dogstatsd/replay/mock.go +++ b/comp/dogstatsd/replay/mock/mock.go @@ -3,33 +3,39 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2016-2021 Datadog, Inc. -package replay +//go:build test + +//nolint:revive // TODO(AML) Fix revive linter +package mock import ( - "context" "sync" + "testing" "time" - "go.uber.org/fx" - "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" ) -type mockTrafficCapture struct { - isRunning bool - sync.RWMutex +// Mock implements mock-specific methods. +type Mock interface { + replay.Component +} + +//nolint:revive // TODO(AML) Fix revive linter +type Requires struct { + T testing.TB } -func newMockTrafficCapture(deps dependencies) Component { +//nolint:revive // TODO(AML) Fix revive linter +func NewTrafficCapture(deps Requires) replay.Component { tc := &mockTrafficCapture{} - deps.Lc.Append(fx.Hook{ - OnStart: tc.configure, - }) return tc } -func (tc *mockTrafficCapture) configure(_ context.Context) error { - return nil +type mockTrafficCapture struct { + isRunning bool + sync.RWMutex } func (tc *mockTrafficCapture) IsOngoing() bool { @@ -65,7 +71,7 @@ func (tc *mockTrafficCapture) RegisterOOBPoolManager(p *packets.PoolManager) err } //nolint:revive // TODO(AML) Fix revive linter -func (tc *mockTrafficCapture) Enqueue(msg *CaptureBuffer) bool { +func (tc *mockTrafficCapture) Enqueue(msg *replay.CaptureBuffer) bool { return true } diff --git a/comp/dogstatsd/server/server.go b/comp/dogstatsd/server/server.go index 4fdf79592afbd5..cb1c469c3f1879 100644 --- a/comp/dogstatsd/server/server.go +++ b/comp/dogstatsd/server/server.go @@ -25,7 +25,7 @@ import ( "github.com/DataDog/datadog-agent/comp/dogstatsd/mapper" "github.com/DataDog/datadog-agent/comp/dogstatsd/packets" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" serverdebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" "github.com/DataDog/datadog-agent/pkg/aggregator" "github.com/DataDog/datadog-agent/pkg/config" diff --git a/comp/dogstatsd/server/server_test.go b/comp/dogstatsd/server/server_test.go index f779f053acbd52..65c9122aabd507 100644 --- a/comp/dogstatsd/server/server_test.go +++ b/comp/dogstatsd/server/server_test.go @@ -29,7 +29,8 @@ import ( "github.com/DataDog/datadog-agent/comp/dogstatsd/listeners" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap/pidmapimpl" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" + replaymock "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/fx-mock" serverdebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug/serverdebugimpl" "github.com/DataDog/datadog-agent/comp/serializer/compression/compressionimpl" @@ -65,7 +66,7 @@ func fulfillDepsWithConfigOverrideAndFeatures(t testing.TB, overrides map[string Features: features, }), fx.Supply(Params{Serverless: false}), - replay.MockModule(), + replaymock.MockModule(), compressionimpl.MockModule(), pidmapimpl.Module(), demultiplexerimpl.FakeSamplerMockModule(), @@ -87,7 +88,7 @@ func fulfillDepsWithConfigYaml(t testing.TB, yaml string) serverDeps { Params: configComponent.Params{ConfFilePath: yaml}, }), fx.Supply(Params{Serverless: false}), - replay.MockModule(), + replaymock.MockModule(), compressionimpl.MockModule(), pidmapimpl.Module(), demultiplexerimpl.FakeSamplerMockModule(), diff --git a/comp/dogstatsd/server/serverless.go b/comp/dogstatsd/server/serverless.go index 28e179e0132009..e38a955af74099 100644 --- a/comp/dogstatsd/server/serverless.go +++ b/comp/dogstatsd/server/serverless.go @@ -12,7 +12,7 @@ import ( logComponentImpl "github.com/DataDog/datadog-agent/comp/core/log/logimpl" "github.com/DataDog/datadog-agent/comp/core/workloadmeta" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap/pidmapimpl" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/impl" "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug/serverdebugimpl" "github.com/DataDog/datadog-agent/pkg/aggregator" "github.com/DataDog/datadog-agent/pkg/config" diff --git a/pkg/cli/subcommands/check/command.go b/pkg/cli/subcommands/check/command.go index 06c5ee33aab295..5f4ef6fe169133 100644 --- a/pkg/cli/subcommands/check/command.go +++ b/pkg/cli/subcommands/check/command.go @@ -54,7 +54,7 @@ import ( "github.com/DataDog/datadog-agent/comp/core/workloadmeta/collectors" "github.com/DataDog/datadog-agent/comp/core/workloadmeta/defaults" "github.com/DataDog/datadog-agent/comp/dogstatsd/pidmap" - "github.com/DataDog/datadog-agent/comp/dogstatsd/replay" + replay "github.com/DataDog/datadog-agent/comp/dogstatsd/replay/def" "github.com/DataDog/datadog-agent/comp/dogstatsd/server" serverdebug "github.com/DataDog/datadog-agent/comp/dogstatsd/serverDebug" "github.com/DataDog/datadog-agent/comp/forwarder" From 7ac9c8b8595235bdf9a3ed457d0095100251c451 Mon Sep 17 00:00:00 2001 From: Gustavo Caso Date: Fri, 24 May 2024 19:49:35 +0200 Subject: [PATCH 32/32] [ASCII-1796] Move list dependency test to python (#25866) Co-authored-by: pgimalac --- .github/CODEOWNERS | 1 + .gitlab/JOBOWNERS | 2 + .gitlab/source_test/golang_deps_diff.yml | 13 ++ ...amd64.txt => dependencies_linux_amd64.txt} | 2 +- ...arm64.txt => dependencies_linux_arm64.txt} | 2 +- cmd/serverless/dependency_list_test.go | 57 -------- tasks/go_deps.py | 135 ++++++++++++++++++ 7 files changed, 153 insertions(+), 59 deletions(-) rename cmd/serverless/{linux_dependencies_amd64.txt => dependencies_linux_amd64.txt} (99%) rename cmd/serverless/{linux_dependencies_arm64.txt => dependencies_linux_arm64.txt} (99%) delete mode 100644 cmd/serverless/dependency_list_test.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5eaecccef6f2ee..4c96f0a4165fcc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -176,6 +176,7 @@ /cmd/otel-agent/ @DataDog/opentelemetry /cmd/process-agent/ @DataDog/processes /cmd/serverless/ @DataDog/serverless +/cmd/serverless/dependencies*.txt @DataDog/serverless @DataDog/agent-shared-components /cmd/serverless-init/ @DataDog/serverless /cmd/system-probe/ @DataDog/ebpf-platform /cmd/system-probe/config/adjust_npm.go @DataDog/ebpf-platform @DataDog/Networks diff --git a/.gitlab/JOBOWNERS b/.gitlab/JOBOWNERS index 7b0e6564393b7e..2f391e4f7e1e36 100644 --- a/.gitlab/JOBOWNERS +++ b/.gitlab/JOBOWNERS @@ -15,6 +15,8 @@ prepare_secagent_ebpf_functional_tests* @DataDog/agent-security # Send count metrics about Golang dependencies golang_deps_send_count_metrics @DataDog/agent-shared-components +# Golang test dependecies diff +golang_deps_test @DataDog/agent-shared-components # Golang dependency diff generation golang_deps_diff @DataDog/ebpf-platform golang_deps_commenter @DataDog/ebpf-platform diff --git a/.gitlab/source_test/golang_deps_diff.yml b/.gitlab/source_test/golang_deps_diff.yml index 28d9c3f5fe4d50..b3bc12c87350eb 100644 --- a/.gitlab/source_test/golang_deps_diff.yml +++ b/.gitlab/source_test/golang_deps_diff.yml @@ -62,3 +62,16 @@ golang_deps_send_count_metrics: # Get API key to send metrics - export DD_API_KEY=$($CI_PROJECT_DIR/tools/ci/aws_ssm_get_wrapper.sh $API_KEY_ORG2_SSM_NAME) - inv -e go-deps.send-count-metrics --git-sha "${CI_COMMIT_SHA}" --git-ref "${CI_COMMIT_REF_NAME}" + +golang_deps_test: + stage: source_test + image: 486234852809.dkr.ecr.us-east-1.amazonaws.com/ci/datadog-agent-buildimages/deb_x64$DATADOG_AGENT_BUILDIMAGES_SUFFIX:$DATADOG_AGENT_BUILDIMAGES + tags: ["arch:amd64"] + rules: + - when: on_success + needs: ["go_deps"] + before_script: + - source /root/.bashrc + - !reference [.retrieve_linux_go_deps] + script: + - inv -e go-deps.test-list diff --git a/cmd/serverless/linux_dependencies_amd64.txt b/cmd/serverless/dependencies_linux_amd64.txt similarity index 99% rename from cmd/serverless/linux_dependencies_amd64.txt rename to cmd/serverless/dependencies_linux_amd64.txt index 6002ca82f2b1ea..afed5e71c0ffd4 100644 --- a/cmd/serverless/linux_dependencies_amd64.txt +++ b/cmd/serverless/dependencies_linux_amd64.txt @@ -1052,4 +1052,4 @@ vendor/golang.org/x/sys/cpu vendor/golang.org/x/text/secure/bidirule vendor/golang.org/x/text/transform vendor/golang.org/x/text/unicode/bidi -vendor/golang.org/x/text/unicode/norm +vendor/golang.org/x/text/unicode/norm \ No newline at end of file diff --git a/cmd/serverless/linux_dependencies_arm64.txt b/cmd/serverless/dependencies_linux_arm64.txt similarity index 99% rename from cmd/serverless/linux_dependencies_arm64.txt rename to cmd/serverless/dependencies_linux_arm64.txt index 9e1eb1c3aff51d..e725dcd55188f6 100644 --- a/cmd/serverless/linux_dependencies_arm64.txt +++ b/cmd/serverless/dependencies_linux_arm64.txt @@ -1050,4 +1050,4 @@ vendor/golang.org/x/net/idna vendor/golang.org/x/text/secure/bidirule vendor/golang.org/x/text/transform vendor/golang.org/x/text/unicode/bidi -vendor/golang.org/x/text/unicode/norm +vendor/golang.org/x/text/unicode/norm \ No newline at end of file diff --git a/cmd/serverless/dependency_list_test.go b/cmd/serverless/dependency_list_test.go deleted file mode 100644 index abd0df4f11d995..00000000000000 --- a/cmd/serverless/dependency_list_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Unless explicitly stated otherwise all files in this repository are licensed -// under the Apache License Version 2.0. -// This product includes software developed at Datadog (https://www.datadoghq.com/). -// Copyright 2016-present Datadog, Inc. - -//go:build test && linux - -package main - -import ( - "fmt" - "os" - "os/exec" - "runtime" - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -const erroMsg = ` -The %s_dependencies_%s.txt file is out of date. -Update the file locally with this content: -%s -` - -func buildDependencyList() (string, error) { - run := "go" - arg0 := "list" - arg1 := "-f" - arg2 := `"{{ join .Deps "\n"}}"` - arg3 := "-tags" - arg4 := "serverless,otlp" - arg5 := "github.com/DataDog/datadog-agent/cmd/serverless" - cmd := exec.Command(run, arg0, arg1, arg2, arg3, arg4, arg5) - - stdout, err := cmd.Output() - return string(stdout), err -} - -// This test is here to add friction to the process of adding dependencies to the serverless binary. -// If you are adding a dependency to the serverless binary, you must update the dependencies.txt file. -// Same for when you remove a dependency. -// Having this test also allow us to better track additions and removals of dependencies for the serverless binary. -func TestImportPackage(t *testing.T) { - dependencyList, err := buildDependencyList() - assert.NoError(t, err) - file := fmt.Sprintf("%s_dependencies_%s.txt", runtime.GOOS, runtime.GOARCH) - - data, err := os.ReadFile(file) - assert.NoError(t, err) - - cleanDependencyList := strings.TrimLeft(dependencyList, "\"") - cleanDependencyList = strings.TrimRight(cleanDependencyList, "\"\n") - cleanDependencyList += "\n" - assert.Equal(t, string(data), cleanDependencyList, fmt.Sprintf(erroMsg, runtime.GOOS, runtime.GOARCH, cleanDependencyList)) -} diff --git a/tasks/go_deps.py b/tasks/go_deps.py index b7bcbc25fd1ad8..70acadbd8c93a2 100644 --- a/tasks/go_deps.py +++ b/tasks/go_deps.py @@ -1,5 +1,7 @@ import datetime +import io import os +from collections import namedtuple from collections.abc import Iterable from invoke.context import Context @@ -90,6 +92,33 @@ def compute_all_count_metrics(ctx: Context, extra_tags: Iterable[str] = ()): return series +def compute_binary_dependencies_list( + ctx: Context, + build: str, + flavor: AgentFlavor, + platform: str, + arch: str, +): + """ + Compute binary import list for the given build/flavor/platform/arch. + """ + goos, goarch = GOOS_MAPPING[platform], GOARCH_MAPPING[arch] + + build_tags = get_default_build_tags(build=build, flavor=flavor, platform=platform) + + env = {"GOOS": goos, "GOARCH": goarch, "CGO_ENABLED": "1"} + cmd = "go list -f '{{ join .Deps \"\\n\"}}'" + + res = ctx.run( + f"{cmd} -tags {','.join(build_tags)}", + env=env, + hide='out', # don't hide errors + ) + assert res + + return res.stdout.strip() + + @task def send_count_metrics( ctx: Context, @@ -119,3 +148,109 @@ def send_count_metrics( print(color_message("Sending metrics to Datadog", "blue")) send_metrics(series=series) print(color_message("Done", "green")) + + +BINARY_TO_TEST = ["serverless"] +MisMacthBinary = namedtuple('failedBinary', ['binary', 'os', 'arch', 'differences']) + + +@task +def test_list( + ctx: Context, +): + """ + Compare the dependencies list for the binaries in BINARY_TO_TEST with the actual dependencies of the binaries. + If the lists do not match, the task will raise an error. + """ + mismatch_binaries = set() + + for binary in BINARY_TO_TEST: + binary_info = BINARIES[binary] + entrypoint = binary_info["entrypoint"] + platforms = binary_info["platforms"] + flavor = binary_info.get("flavor", AgentFlavor.base) + build = binary_info.get("build", binary) + + with ctx.cd(entrypoint): + for platform in platforms: + platform, arch = platform.split("/") + + goos, goarch = GOOS_MAPPING[platform], GOARCH_MAPPING[arch] + + filename = os.path.join(ctx.cwd, f"dependencies_{goos}_{goarch}.txt") + if not os.path.isfile(filename): + print( + f"File {filename} does not exist. To execute the dependencies list check for the {binary} binary, please run the task `inv -e go-deps.generate --binaries {binary}" + ) + continue + + deps_file = open(filename) + deps = deps_file.read() + deps_file.close() + + list = compute_binary_dependencies_list(ctx, build, flavor, platform, arch) + + if list != deps: + new_dependencies_lines = len(list.splitlines()) + recorded_dependencies_lines = len(deps.splitlines()) + + mismatch_binaries.add( + MisMacthBinary(binary, goos, goarch, new_dependencies_lines - recorded_dependencies_lines) + ) + + if len(mismatch_binaries) > 0: + message = io.StringIO() + + for mismatch_binary in mismatch_binaries: + if mismatch_binary.differences > 0: + message.write( + color_message( + f"You added some dependencies to {mismatch_binary.binary} ({mismatch_binary.os}/{mismatch_binary.arch}). Adding new dependencies to the binary increases its size. Do we really need to add this dependency?\n", + "red", + ) + ) + else: + message.write( + color_message( + f"You removed some dependencies from {mismatch_binary.binary} ({mismatch_binary.os}/{mismatch_binary.arch}). Congratulations!\n", + "green", + ) + ) + + message.write( + color_message( + "To fix this check, please run `inv -e go-deps.generate`", + "orange", + ) + ) + + raise Exit( + code=1, + message=message.getvalue(), + ) + + +@task +def generate( + ctx: Context, +): + for binary in BINARY_TO_TEST: + binary_info = BINARIES[binary] + entrypoint = binary_info["entrypoint"] + platforms = binary_info["platforms"] + flavor = binary_info.get("flavor", AgentFlavor.base) + build = binary_info.get("build", binary) + + with ctx.cd(entrypoint): + for platform in platforms: + platform, arch = platform.split("/") + + goos, goarch = GOOS_MAPPING[platform], GOARCH_MAPPING[arch] + + filename = os.path.join(ctx.cwd, f"dependencies_{goos}_{goarch}.txt") + + list = compute_binary_dependencies_list(ctx, build, flavor, platform, arch) + + f = open(filename, "w") + f.write(list) + f.close()
binaryosarchchange