diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index 37a222f49..2a3c287f1 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -87,6 +87,17 @@ steps: - build/*.xml - build/coverage*.out + - label: ":smartbear-testexecute: Run FIPS unit tests" + key: unit-test-fips + command: ".buildkite/scripts/unit_test.sh" + env: + FIPS: "true" + agents: + provider: "gcp" + artifact_paths: + - build/*.xml + - build/coverage*.out + - label: ":smartbear-testexecute: Run unit tests: MacOS 13" key: unit-test-macos-13 command: ".buildkite/scripts/unit_test.sh" diff --git a/.buildkite/scripts/unit_test.sh b/.buildkite/scripts/unit_test.sh index 5a2eb87ae..a60877730 100755 --- a/.buildkite/scripts/unit_test.sh +++ b/.buildkite/scripts/unit_test.sh @@ -9,4 +9,8 @@ add_bin_path with_go echo "Starting the unit tests..." -make test-unit junit-report +if [[ ${FIPS:-} == "true" ]]; then + make test-unit-fips junit-report +else + make test-unit junit-report +fi diff --git a/Makefile b/Makefile index 58d883ac2..688f6d1fc 100644 --- a/Makefile +++ b/Makefile @@ -221,9 +221,16 @@ test: prepare-test-context ## - Run all tests test-release: ## - Check that all release binaries are created ./.buildkite/scripts/test-release.sh $(DEFAULT_VERSION) +# If FIPS=true unit tests need microsoft/go + OpenSSL with FIPS .PHONY: test-unit test-unit: prepare-test-context ## - Run unit tests only - set -o pipefail; go test ${GO_TEST_FLAG} -tags=$(GOBUILDTAGS) -v -race -coverprofile=build/coverage-${OS_NAME}.out ./... | tee build/test-unit-${OS_NAME}.out + set -o pipefail; ${GOFIPSEXPERIMENT} go test ${GO_TEST_FLAG} -tags=$(GOBUILDTAGS) -v -race -coverprofile=build/coverage-${OS_NAME}.out ./... | tee build/test-unit-${OS_NAME}.out + +# FIPS unit tests are meant to use go v1.24 to check FIPS compliance. +# This check is very strict, and should be thought of as a static-code analysis tool. +.PHONY: test-unit-fips +test-unit-fips: prepare-test-context ## - Run unit tests with go 1.24's fips140=only for testing + set -o pipefail; GOFIPS140=latest GODEBUG=fips140=only go test ${GO_TEST_FLAG} -tags=$(GOBUILDTAGS) -v -race -coverprofile=build/coverage-${OS_NAME}.out ./... | tee build/test-unit-fips-${OS_NAME}.out .PHONY: benchmark benchmark: prepare-test-context install-benchstat ## - Run benchmark tests only diff --git a/internal/pkg/config/config_fips_test.go b/internal/pkg/config/config_fips_test.go new file mode 100644 index 000000000..059d4de43 --- /dev/null +++ b/internal/pkg/config/config_fips_test.go @@ -0,0 +1,30 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build !integration && requirefips + +package config + +import ( + "crypto/tls" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-agent-libs/transport/tlscommon" +) + +func TestTLSDefaults(t *testing.T) { + c, err := LoadFile(filepath.Join("testdata", "tls.yml")) + require.NoError(t, err) + require.NotNil(t, c.Output.Elasticsearch.TLS) + + common, err := tlscommon.LoadTLSConfig(c.Output.Elasticsearch.TLS) + require.NoError(t, err) + cfg := common.ToConfig() + assert.Equal(t, uint16(tls.VersionTLS12), cfg.MinVersion) + assert.Equal(t, uint16(tls.VersionTLS13), cfg.MaxVersion) +} diff --git a/internal/pkg/config/config_nofips_test.go b/internal/pkg/config/config_nofips_test.go new file mode 100644 index 000000000..427efb0a0 --- /dev/null +++ b/internal/pkg/config/config_nofips_test.go @@ -0,0 +1,42 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build !integration && !requirefips + +package config + +import ( + "crypto/tls" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-agent-libs/transport/tlscommon" +) + +func TestTLSDefaults(t *testing.T) { + c, err := LoadFile(filepath.Join("testdata", "tls.yml")) + require.NoError(t, err) + require.NotNil(t, c.Output.Elasticsearch.TLS) + + common, err := tlscommon.LoadTLSConfig(c.Output.Elasticsearch.TLS) + require.NoError(t, err) + cfg := common.ToConfig() + assert.Equal(t, uint16(tls.VersionTLS11), cfg.MinVersion) + assert.Equal(t, uint16(tls.VersionTLS13), cfg.MaxVersion) +} + +func TestTLS10(t *testing.T) { + c, err := LoadFile(filepath.Join("testdata", "tls10.yml")) + require.NoError(t, err) + require.NotNil(t, c.Output.Elasticsearch.TLS) + + common, err := tlscommon.LoadTLSConfig(c.Output.Elasticsearch.TLS) + require.NoError(t, err) + cfg := common.ToConfig() + assert.Equal(t, uint16(tls.VersionTLS10), cfg.MinVersion) + assert.Equal(t, uint16(tls.VersionTLS10), cfg.MaxVersion) +} diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go index 3a6d02fad..07dc1d934 100644 --- a/internal/pkg/config/config_test.go +++ b/internal/pkg/config/config_test.go @@ -7,13 +7,11 @@ package config import ( - "crypto/tls" "path/filepath" "sync/atomic" "testing" "time" - "github.com/elastic/elastic-agent-libs/transport/tlscommon" testlog "github.com/elastic/fleet-server/v7/internal/pkg/testing/log" "github.com/gofrs/uuid" @@ -630,27 +628,3 @@ func TestDeprecationWarnings(t *testing.T) { require.NoError(t, err) assert.Equal(t, uint64(3), logCount.Load(), "Expected 3 log messages") } - -func TestTLSDefaults(t *testing.T) { - c, err := LoadFile(filepath.Join("testdata", "tls.yml")) - require.NoError(t, err) - require.NotNil(t, c.Output.Elasticsearch.TLS) - - common, err := tlscommon.LoadTLSConfig(c.Output.Elasticsearch.TLS) - require.NoError(t, err) - cfg := common.ToConfig() - assert.Equal(t, uint16(tls.VersionTLS11), cfg.MinVersion) - assert.Equal(t, uint16(tls.VersionTLS13), cfg.MaxVersion) -} - -func TestTLS10(t *testing.T) { - c, err := LoadFile(filepath.Join("testdata", "tls10.yml")) - require.NoError(t, err) - require.NotNil(t, c.Output.Elasticsearch.TLS) - - common, err := tlscommon.LoadTLSConfig(c.Output.Elasticsearch.TLS) - require.NoError(t, err) - cfg := common.ToConfig() - assert.Equal(t, uint16(tls.VersionTLS10), cfg.MinVersion) - assert.Equal(t, uint16(tls.VersionTLS10), cfg.MaxVersion) -} diff --git a/internal/pkg/config/output_fips_test.go b/internal/pkg/config/output_fips_test.go new file mode 100644 index 000000000..38afeb605 --- /dev/null +++ b/internal/pkg/config/output_fips_test.go @@ -0,0 +1,127 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build !integration && requirefips + +package config + +import ( + "crypto/tls" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-agent-libs/transport/tlscommon" + testlog "github.com/elastic/fleet-server/v7/internal/pkg/testing/log" + "github.com/elastic/go-elasticsearch/v8" +) + +func TestToESConfigTLS(t *testing.T) { + testcases := map[string]struct { + cfg Elasticsearch + result elasticsearch.Config + }{ + "https": { + cfg: Elasticsearch{ + Protocol: "https", + Hosts: []string{"localhost:9200", "other-host:9200"}, + ServiceToken: "test-token", + Headers: map[string]string{ + "X-Custom-Header": "Header-Value", + }, + MaxRetries: 6, + MaxConnPerHost: 256, + Timeout: 120 * time.Second, + TLS: &tlscommon.Config{ + VerificationMode: tlscommon.VerifyNone, + }, + }, + result: elasticsearch.Config{ + Addresses: []string{"https://localhost:9200", "https://other-host:9200"}, + ServiceToken: "test-token", + Header: http.Header{"X-Custom-Header": {"Header-Value"}}, + MaxRetries: 6, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // test case + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{}, + CurvePreferences: []tls.CurveID{}, + }, + TLSHandshakeTimeout: 10 * time.Second, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 32, + MaxConnsPerHost: 256, + IdleConnTimeout: 60 * time.Second, + ResponseHeaderTimeout: 120 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, + }, + "mixed-https": { + cfg: Elasticsearch{ + Protocol: "http", + Hosts: []string{"localhost:9200", "https://other-host:9200"}, + ServiceToken: "test-token", + Headers: map[string]string{ + "X-Custom-Header": "Header-Value", + }, + MaxRetries: 6, + MaxConnPerHost: 256, + Timeout: 120 * time.Second, + TLS: &tlscommon.Config{ + VerificationMode: tlscommon.VerifyNone, + }, + }, + result: elasticsearch.Config{ + Addresses: []string{"http://localhost:9200", "https://other-host:9200"}, + ServiceToken: "test-token", + Header: http.Header{"X-Custom-Header": {"Header-Value"}}, + MaxRetries: 6, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // test case + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{}, + CurvePreferences: []tls.CurveID{}, + }, + TLSHandshakeTimeout: 10 * time.Second, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 32, + MaxConnsPerHost: 256, + IdleConnTimeout: 60 * time.Second, + ResponseHeaderTimeout: 120 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, + }, + } + + copts := cmp.Options{ + cmpopts.IgnoreUnexported(http.Transport{}), + cmpopts.IgnoreFields(http.Transport{}, "DialContext"), + cmpopts.IgnoreUnexported(tls.Config{}), //nolint:gosec //test case + } + + for name, test := range testcases { + t.Run(name, func(t *testing.T) { + _ = testlog.SetLogger(t) + res, err := test.cfg.ToESConfig(false) + require.NoError(t, err) + + // cmp.Diff can't handle function pointers. + res.Transport.(*http.Transport).Proxy = nil + + test.result.Header.Set("X-elastic-product-origin", "fleet") + assert.True(t, cmp.Equal(test.result, res, copts...), "mismatch (-want +got)\n%s", cmp.Diff(test.result, res, copts...)) + }) + } +} diff --git a/internal/pkg/config/output_nofips_test.go b/internal/pkg/config/output_nofips_test.go new file mode 100644 index 000000000..1fea45cec --- /dev/null +++ b/internal/pkg/config/output_nofips_test.go @@ -0,0 +1,127 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +//go:build !integration && !requirefips + +package config + +import ( + "crypto/tls" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/elastic-agent-libs/transport/tlscommon" + testlog "github.com/elastic/fleet-server/v7/internal/pkg/testing/log" + "github.com/elastic/go-elasticsearch/v8" +) + +func TestToESConfigTLS(t *testing.T) { + testcases := map[string]struct { + cfg Elasticsearch + result elasticsearch.Config + }{ + "https": { + cfg: Elasticsearch{ + Protocol: "https", + Hosts: []string{"localhost:9200", "other-host:9200"}, + ServiceToken: "test-token", + Headers: map[string]string{ + "X-Custom-Header": "Header-Value", + }, + MaxRetries: 6, + MaxConnPerHost: 256, + Timeout: 120 * time.Second, + TLS: &tlscommon.Config{ + VerificationMode: tlscommon.VerifyNone, + }, + }, + result: elasticsearch.Config{ + Addresses: []string{"https://localhost:9200", "https://other-host:9200"}, + ServiceToken: "test-token", + Header: http.Header{"X-Custom-Header": {"Header-Value"}}, + MaxRetries: 6, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // test case + MinVersion: tls.VersionTLS11, + MaxVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{}, + CurvePreferences: []tls.CurveID{}, + }, + TLSHandshakeTimeout: 10 * time.Second, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 32, + MaxConnsPerHost: 256, + IdleConnTimeout: 60 * time.Second, + ResponseHeaderTimeout: 120 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, + }, + "mixed-https": { + cfg: Elasticsearch{ + Protocol: "http", + Hosts: []string{"localhost:9200", "https://other-host:9200"}, + ServiceToken: "test-token", + Headers: map[string]string{ + "X-Custom-Header": "Header-Value", + }, + MaxRetries: 6, + MaxConnPerHost: 256, + Timeout: 120 * time.Second, + TLS: &tlscommon.Config{ + VerificationMode: tlscommon.VerifyNone, + }, + }, + result: elasticsearch.Config{ + Addresses: []string{"http://localhost:9200", "https://other-host:9200"}, + ServiceToken: "test-token", + Header: http.Header{"X-Custom-Header": {"Header-Value"}}, + MaxRetries: 6, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, //nolint:gosec // test case + MinVersion: tls.VersionTLS11, + MaxVersion: tls.VersionTLS13, + Certificates: []tls.Certificate{}, + CurvePreferences: []tls.CurveID{}, + }, + TLSHandshakeTimeout: 10 * time.Second, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 32, + MaxConnsPerHost: 256, + IdleConnTimeout: 60 * time.Second, + ResponseHeaderTimeout: 120 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + }, + }, + } + + copts := cmp.Options{ + cmpopts.IgnoreUnexported(http.Transport{}), + cmpopts.IgnoreFields(http.Transport{}, "DialContext"), + cmpopts.IgnoreUnexported(tls.Config{}), //nolint:gosec //test case + } + + for name, test := range testcases { + t.Run(name, func(t *testing.T) { + _ = testlog.SetLogger(t) + res, err := test.cfg.ToESConfig(false) + require.NoError(t, err) + + // cmp.Diff can't handle function pointers. + res.Transport.(*http.Transport).Proxy = nil + + test.result.Header.Set("X-elastic-product-origin", "fleet") + assert.True(t, cmp.Equal(test.result, res, copts...), "mismatch (-want +got)\n%s", cmp.Diff(test.result, res, copts...)) + }) + } +} diff --git a/internal/pkg/config/output_test.go b/internal/pkg/config/output_test.go index 11cc2c699..0cff9c4ae 100644 --- a/internal/pkg/config/output_test.go +++ b/internal/pkg/config/output_test.go @@ -25,8 +25,6 @@ import ( "github.com/stretchr/testify/require" "github.com/elastic/go-elasticsearch/v8" - - "github.com/elastic/elastic-agent-libs/transport/tlscommon" ) func TestToESConfig(t *testing.T) { @@ -113,82 +111,6 @@ func TestToESConfig(t *testing.T) { }, }, }, - "https": { - cfg: Elasticsearch{ - Protocol: "https", - Hosts: []string{"localhost:9200", "other-host:9200"}, - ServiceToken: "test-token", - Headers: map[string]string{ - "X-Custom-Header": "Header-Value", - }, - MaxRetries: 6, - MaxConnPerHost: 256, - Timeout: 120 * time.Second, - TLS: &tlscommon.Config{ - VerificationMode: tlscommon.VerifyNone, - }, - }, - result: elasticsearch.Config{ - Addresses: []string{"https://localhost:9200", "https://other-host:9200"}, - ServiceToken: "test-token", - Header: http.Header{"X-Custom-Header": {"Header-Value"}}, - MaxRetries: 6, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //nolint:gosec // test case - MinVersion: tls.VersionTLS11, - MaxVersion: tls.VersionTLS13, - Certificates: []tls.Certificate{}, - CurvePreferences: []tls.CurveID{}, - }, - TLSHandshakeTimeout: 10 * time.Second, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 32, - MaxConnsPerHost: 256, - IdleConnTimeout: 60 * time.Second, - ResponseHeaderTimeout: 120 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - }, - }, - "mixed-https": { - cfg: Elasticsearch{ - Protocol: "http", - Hosts: []string{"localhost:9200", "https://other-host:9200"}, - ServiceToken: "test-token", - Headers: map[string]string{ - "X-Custom-Header": "Header-Value", - }, - MaxRetries: 6, - MaxConnPerHost: 256, - Timeout: 120 * time.Second, - TLS: &tlscommon.Config{ - VerificationMode: tlscommon.VerifyNone, - }, - }, - result: elasticsearch.Config{ - Addresses: []string{"http://localhost:9200", "https://other-host:9200"}, - ServiceToken: "test-token", - Header: http.Header{"X-Custom-Header": {"Header-Value"}}, - MaxRetries: 6, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //nolint:gosec // test case - MinVersion: tls.VersionTLS11, - MaxVersion: tls.VersionTLS13, - Certificates: []tls.Certificate{}, - CurvePreferences: []tls.CurveID{}, - }, - TLSHandshakeTimeout: 10 * time.Second, - MaxIdleConns: 100, - MaxIdleConnsPerHost: 32, - MaxConnsPerHost: 256, - IdleConnTimeout: 60 * time.Second, - ResponseHeaderTimeout: 120 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }, - }, - }, } copts := cmp.Options{