diff --git a/Makefile b/Makefile index e511a5b6c..b5bbe261c 100644 --- a/Makefile +++ b/Makefile @@ -74,6 +74,43 @@ test: ### run test go test -v -cover -race ./... .PHONY: test +FUZZ_ROOT ?= internal +FUZZTIME ?= 30s + +fuzz-list: ### list all fuzz targets as ' ' + @set -eu; \ + find $(FUZZ_ROOT) -name '*fuzz_test.go' -exec dirname {} \; | sort -u | while read -r dir; do \ + pkg="./$$dir"; \ + go test "$$pkg" -list '^Fuzz' 2>/dev/null | grep '^Fuzz' | while read -r target; do \ + echo "$$pkg $$target"; \ + done; \ + done +.PHONY: fuzz-list + +fuzz-one: ### run one fuzz target, e.g. make fuzz-one PKG=./internal/usecase/devices TARGET=FuzzParseInterval FUZZTIME=30s + @if [ -z "$(PKG)" ] || [ -z "$(TARGET)" ]; then \ + echo "usage: make fuzz-one PKG=./path TARGET=FuzzTarget [FUZZTIME=30s]"; \ + exit 1; \ + fi + go test "$(PKG)" -run=^$$ -fuzz="^$(TARGET)$$" -fuzztime="$(FUZZTIME)" +.PHONY: fuzz-one + +fuzz-smoke: ### run all fuzz targets once (quick CI smoke) + $(MAKE) fuzz-all FUZZTIME=1x +.PHONY: fuzz-smoke + +fuzz-all: ### run all fuzz targets sequentially with FUZZTIME per target + @set -eu; \ + find $(FUZZ_ROOT) -name '*fuzz_test.go' -exec dirname {} \; | sort -u | while read -r dir; do \ + pkg="./$$dir"; \ + targets=$$(go test "$$pkg" -list '^Fuzz' 2>/dev/null | grep '^Fuzz' || true); \ + for target in $$targets; do \ + echo "==> $$pkg $$target (FUZZTIME=$(FUZZTIME))"; \ + go test "$$pkg" -run=^$$ -fuzz="^$${target}$$" -fuzztime="$(FUZZTIME)"; \ + done; \ + done +.PHONY: fuzz-all + integration-test: ### run integration-test go clean -testcache && go test -v ./integration-test/... .PHONY: integration-test diff --git a/README.md b/README.md index ddfef86f9..9d154cb83 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,56 @@ Console automatically generates OpenAPI documentation when running in debug mode - Windows: `docker run --rm -v ${pwd}:/app -w /app golangci/golangci-lint:latest golangci-lint run --config=./.golangci.yml -v` - Unix: `docker run --rm -v .:/app -w /app golangci/golangci-lint:latest golangci-lint run --config=./.golangci.yml -v` +## Fuzz Testing + +Console includes Go fuzz tests across multiple packages and targets. For full coverage details, seed corpus strategy, and CI guidance, see wiki: [Fuzz Testing](https://github.com/device-management-toolkit/console/wiki/Fuzz-Testing) + +### Prerequisite + +The Makefile includes `.env`, so ensure it exists before running make commands: + +```sh +cp .env.example .env +``` + +### List all fuzz targets + +```sh +make fuzz-list +``` + +Output format: + +```text +./internal/usecase/devices FuzzParseInterval +./internal/entity/dto/v1 FuzzDeviceJSONProcessing +... +``` + +### Run a single fuzz target (recommended local workflow) + +Go fuzzing runs one fuzz target at a time. Use `fuzz-one` for focused debugging and development: + +```sh +make fuzz-one PKG=./internal/usecase/devices TARGET=FuzzParseInterval FUZZTIME=30s +``` + +### Run a quick all-target smoke pass + +Runs each fuzz target once (`-fuzztime=1x`): + +```sh +make fuzz-smoke +``` + +### Run all fuzz targets with time budget per target + +```sh +make fuzz-all FUZZTIME=2m +``` + +This executes all discovered fuzz targets sequentially and is suitable for scheduled CI jobs. + ## Additional Resources diff --git a/internal/certificates/generate_fuzz_test.go b/internal/certificates/generate_fuzz_test.go new file mode 100644 index 000000000..04285085e --- /dev/null +++ b/internal/certificates/generate_fuzz_test.go @@ -0,0 +1,152 @@ +package certificates + +import ( + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + mathrand "math/rand" + "strings" + "testing" + "time" +) + +func FuzzParseCertificateFromPEM(f *testing.F) { + validCertPEM, validKeyPEM := generateFuzzPEMCertificate(f, 1) + otherCertPEM, otherKeyPEM := generateFuzzPEMCertificate(f, 2) + truncatedCertPEM := validCertPEM[:len(validCertPEM)/2] + truncatedKeyPEM := validKeyPEM[:len(validKeyPEM)/2] + corruptedCertPEM := corruptPEMBody(validCertPEM) + corruptedKeyPEM := corruptPEMBody(validKeyPEM) + + seedInputs := []struct { + certPEM string + keyPEM string + }{ + {certPEM: validCertPEM, keyPEM: validKeyPEM}, + {certPEM: otherCertPEM, keyPEM: otherKeyPEM}, + {certPEM: validCertPEM, keyPEM: otherKeyPEM}, + {certPEM: truncatedCertPEM, keyPEM: validKeyPEM}, + {certPEM: validCertPEM, keyPEM: truncatedKeyPEM}, + {certPEM: corruptedCertPEM, keyPEM: validKeyPEM}, + {certPEM: validCertPEM, keyPEM: corruptedKeyPEM}, + {certPEM: "", keyPEM: ""}, + {certPEM: "invalid-pem", keyPEM: validKeyPEM}, + {certPEM: invalidBase64PEM("CERTIFICATE"), keyPEM: validKeyPEM}, + {certPEM: validCertPEM, keyPEM: "invalid-key"}, + {certPEM: validCertPEM, keyPEM: invalidBase64PEM("RSA PRIVATE KEY")}, + {certPEM: validKeyPEM, keyPEM: validCertPEM}, + {certPEM: "前置\n" + validCertPEM + "後置", keyPEM: "🔐\n" + validKeyPEM}, + {certPEM: strings.Repeat("A", 256), keyPEM: strings.Repeat("B", 256)}, + } + + for _, input := range seedInputs { + f.Add(input.certPEM, input.keyPEM) + } + + f.Fuzz(func(t *testing.T, certPEM, keyPEM string) { + firstCert, firstKey, firstErr := ParseCertificateFromPEM(certPEM, keyPEM) + secondCert, secondKey, secondErr := ParseCertificateFromPEM(certPEM, keyPEM) + + if (firstErr == nil) != (secondErr == nil) { + t.Fatalf("ParseCertificateFromPEM error mismatch for cert len=%d key len=%d: first=%v second=%v", len(certPEM), len(keyPEM), firstErr, secondErr) + } + + if firstErr != nil { + verifyParseCertError(t, certPEM, keyPEM, firstErr, secondErr, firstCert, secondCert, firstKey, secondKey) + + return + } + + verifyParseCertSuccess(t, certPEM, keyPEM, firstCert, secondCert, firstKey, secondKey) + }) +} + +func verifyParseCertError(t *testing.T, certPEM, keyPEM string, firstErr, secondErr error, firstCert, secondCert *x509.Certificate, firstKey, secondKey *rsa.PrivateKey) { + t.Helper() + + if firstErr.Error() != secondErr.Error() { + t.Fatalf("ParseCertificateFromPEM error text mismatch for cert len=%d key len=%d: first=%q second=%q", len(certPEM), len(keyPEM), firstErr.Error(), secondErr.Error()) + } + + if firstCert != nil || secondCert != nil || firstKey != nil || secondKey != nil { + t.Fatalf("ParseCertificateFromPEM returned parsed data alongside an error for cert len=%d key len=%d", len(certPEM), len(keyPEM)) + } +} + +func verifyParseCertSuccess(t *testing.T, certPEM, keyPEM string, firstCert, secondCert *x509.Certificate, firstKey, secondKey *rsa.PrivateKey) { + t.Helper() + + if firstCert == nil || secondCert == nil || firstKey == nil || secondKey == nil { + t.Fatalf("ParseCertificateFromPEM returned nil data without an error for cert len=%d key len=%d", len(certPEM), len(keyPEM)) + + return + } + + if firstCert.SerialNumber.Cmp(secondCert.SerialNumber) != 0 { + t.Fatalf("ParseCertificateFromPEM serial number mismatch for cert len=%d key len=%d", len(certPEM), len(keyPEM)) + } + + if firstCert.Subject.CommonName != secondCert.Subject.CommonName { + t.Fatalf("ParseCertificateFromPEM common name mismatch for cert len=%d key len=%d: first=%q second=%q", len(certPEM), len(keyPEM), firstCert.Subject.CommonName, secondCert.Subject.CommonName) + } + + if firstKey.E != secondKey.E || firstKey.N.Cmp(secondKey.N) != 0 || firstKey.D.Cmp(secondKey.D) != 0 { + t.Fatalf("ParseCertificateFromPEM returned different private keys for cert len=%d key len=%d", len(certPEM), len(keyPEM)) + } +} + +func invalidBase64PEM(blockType string) string { + return "-----BEGIN " + blockType + "-----\n!!!!\n-----END " + blockType + "-----\n" +} + +func corruptPEMBody(input string) string { + block, _ := pem.Decode([]byte(input)) + if block == nil || len(block.Bytes) == 0 { + return input + } + + corrupted := append([]byte(nil), block.Bytes...) + index := len(corrupted) / 2 + corrupted[index] ^= 0xff + + return string(pem.EncodeToMemory(&pem.Block{Type: block.Type, Bytes: corrupted})) +} + +func generateFuzzPEMCertificate(tb testing.TB, seed int64) (certPEM, keyPEM string) { + tb.Helper() + + rng := mathrand.New(mathrand.NewSource(seed)) + + privateKey, err := rsa.GenerateKey(rng, 1024) + if err != nil { + tb.Fatalf("failed to generate RSA key: %v", err) + } + + serialNumber := new(big.Int).SetInt64(rng.Int63()) + + fixedTime := time.Date(2099, 1, 1, 0, 0, 0, 0, time.UTC) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + CommonName: "fuzz-cert", + Organization: []string{"console"}, + }, + NotBefore: fixedTime.Add(-time.Hour), + NotAfter: fixedTime.Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + BasicConstraintsValid: true, + } + + certBytes, err := x509.CreateCertificate(rng, &template, &template, &privateKey.PublicKey, privateKey) + if err != nil { + tb.Fatalf("failed to create certificate: %v", err) + } + + certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})) + keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})) + + return certPEM, keyPEM +} diff --git a/internal/entity/dto/v1/json_fuzz_test.go b/internal/entity/dto/v1/json_fuzz_test.go new file mode 100644 index 000000000..a469fa424 --- /dev/null +++ b/internal/entity/dto/v1/json_fuzz_test.go @@ -0,0 +1,288 @@ +package dto + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/go-playground/validator/v10" +) + +func FuzzDeviceJSONProcessing(f *testing.F) { + seedInputs := []string{ + `{}`, + `{"guid":"ABC-123","username":"admin","password":"P@ssw0rd","mpspassword":"秘密","mebxpassword":"🔐pw","tags":["a","b"],"lastConnected":"2024-01-02T03:04:05Z"}`, + `{"guid":null,"tags":null,"lastConnected":null}`, + `{"guid":123,"username":true,"tags":"not-an-array","lastConnected":"not-a-time"}`, + fmt.Sprintf(`{"guid":"huge","tags":[%s]}`, quotedArray("tag", 4096)), + fmt.Sprintf(`{"guid":"nested","junk":%s}`, nestedJSONObject()), + `{"guid":"unicode","username":"用戶🙂","password":"päss\u0000秘密","mpspassword":"пароль","lastConnected":"9999-12-31T23:59:59+14:00"}`, + `{"guid":"year-zero","lastConnected":"0000-01-01T00:00:00Z"}`, + } + + for _, input := range seedInputs { + f.Add(input) + } + + v := validator.New() + v.SetTagName("binding") + + f.Fuzz(func(t *testing.T, payload string) { + fuzzJSONAndValidate(t, payload, func() *Device { return &Device{} }, func(value *Device) error { + return v.Struct(value) + }) + }) +} + +func FuzzProfileJSONProcessing(f *testing.F) { + seedInputs := []string{ + `{}`, + `{"profileName":"profile-1","activation":"acmactivate","amtPassword":"P@ssw0rd!","mebxPassword":"MebxP@ss!","generateRandomPassword":false,"generateRandomMEBxPassword":false,"dhcpEnabled":true,"wifiConfigs":[{"priority":1,"profileName":"wifi-1","profileProfileName":"profile-1"}],"ciraConfigName":"cira-1","ieee8021xProfileName":"ieee-1","tags":["a","b"]}`, + `{"profileName":"partial","ciraConfigName":null,"ieee8021xProfileName":null,"tags":null,"wifiConfigs":null}`, + `{"profileName":123,"tlsMode":"bad","dhcpEnabled":"false","wifiConfigs":"not-an-array"}`, + fmt.Sprintf(`{"profileName":"huge","activation":"ccmactivate","generateRandomPassword":true,"generateRandomMEBxPassword":true,"tags":[%s],"wifiConfigs":[%s]}`, quotedArray("tag", 4096), repeatedJSON(`{"priority":1,"profileName":"wifi","profileProfileName":"profile"}`, 512)), + fmt.Sprintf(`{"profileName":"nested","activation":"ccmactivate","generateRandomPassword":true,"generateRandomMEBxPassword":true,"junk":%s}`, nestedJSONObject()), + `{"profileName":"contradictory","activation":"ccmactivate","generateRandomPassword":false,"amtPassword":"","generateRandomMEBxPassword":false,"mebxPassword":"","ciraConfigName":"cira","tlsMode":1,"dhcpEnabled":false,"wifiConfigs":[{"priority":1,"profileName":"wifi-1","profileProfileName":"profile-1"}]}`, + `{"profileName":"unicode","activation":"acmactivate","amtPassword":"päss🙂秘密!","mebxPassword":"пароль🔐!","generateRandomPassword":false,"generateRandomMEBxPassword":false,"tags":["日本","\u0000","🙂"]}`, + } + + for _, input := range seedInputs { + f.Add(input) + } + + v := newProfileValidatorForFuzz() + + f.Fuzz(func(t *testing.T, payload string) { + fuzzJSONAndValidate(t, payload, func() *Profile { return &Profile{} }, func(value *Profile) error { + return v.Struct(value) + }) + }) +} + +func FuzzDomainJSONProcessing(f *testing.F) { + seedInputs := []string{ + `{}`, + `{"profileName":"domain-1","domainSuffix":"example.com","provisioningCert":"cert","provisioningCertStorageFormat":"string","provisioningCertPassword":"P@ssw0rd","expirationDate":"2024-01-02T03:04:05Z"}`, + `{"profileName":null,"provisioningCert":null,"expirationDate":null}`, + `{"profileName":123,"provisioningCertPassword":false,"expirationDate":"not-a-time"}`, + fmt.Sprintf(`{"profileName":"nested","domainSuffix":"example.com","provisioningCert":"cert","provisioningCertStorageFormat":"string","provisioningCertPassword":"pw","junk":%s}`, nestedJSONObject()), + `{"profileName":"unicode_日本","domainSuffix":"例え.テスト","provisioningCert":"cert","provisioningCertStorageFormat":"string","provisioningCertPassword":"päss\u0000秘密🔐","expirationDate":"2016-12-31T23:59:60Z"}`, + `{"profileName":"year-zero","domainSuffix":"example.org","provisioningCert":"cert","provisioningCertStorageFormat":"string","provisioningCertPassword":"pw","expirationDate":"0000-01-01T00:00:00Z"}`, + `{"profileName":"year-max","domainSuffix":"example.org","provisioningCert":"cert","provisioningCertStorageFormat":"string","provisioningCertPassword":"pw","expirationDate":"9999-12-31T23:59:59+14:00"}`, + } + + for _, input := range seedInputs { + f.Add(input) + } + + v := validator.New() + v.SetTagName("binding") + _ = v.RegisterValidation("alphanumhyphenunderscore", ValidateAlphaNumHyphenUnderscore) + + f.Fuzz(func(t *testing.T, payload string) { + fuzzJSONAndValidate(t, payload, func() *Domain { return &Domain{} }, func(value *Domain) error { + return v.Struct(value) + }) + }) +} + +func FuzzCIRAConfigJSONProcessing(f *testing.F) { + seedInputs := []string{ + `{}`, + `{"configName":"cira-1","mpsServerAddress":"https://example.com","mpsPort":4433,"username":"admin","password":"P@ssw0rd","commonName":"example.com","serverAddressFormat":201,"authMethod":2,"mpsRootCertificate":"cert"}`, + `{"configName":null,"mpsServerAddress":null,"password":null}`, + `{"mpsPort":"4433","serverAddressFormat":"201","authMethod":"2"}`, + fmt.Sprintf(`{"configName":"nested","mpsServerAddress":"https://example.com","mpsPort":4433,"username":"admin","password":"pw","commonName":"example.com","serverAddressFormat":201,"authMethod":2,"mpsRootCertificate":"cert","junk":%s}`, nestedJSONObject()), + `{"configName":"unicode","mpsServerAddress":"https://例え.テスト","mpsPort":65535,"username":"用戶🙂","password":"päss\u0000秘密🔐","commonName":"例え.テスト","serverAddressFormat":999,"authMethod":99,"mpsRootCertificate":"cert"}`, + } + + for _, input := range seedInputs { + f.Add(input) + } + + v := validator.New() + v.SetTagName("binding") + _ = v.RegisterValidation("alphanumhyphenunderscore", ValidateAlphaNumHyphenUnderscore) + + f.Fuzz(func(t *testing.T, payload string) { + fuzzJSONAndValidate(t, payload, func() *CIRAConfig { return &CIRAConfig{} }, func(value *CIRAConfig) error { + return v.Struct(value) + }) + }) +} + +func FuzzWirelessConfigJSONProcessing(f *testing.F) { + seedInputs := []string{ + `{}`, + `{"profileName":"wifi-1","authenticationMethod":7,"encryptionMethod":4,"ssid":"ssid","pskPassphrase":"P@ssw0rd","linkPolicy":[1,2],"ieee8021xProfileName":"ieee-1"}`, + `{"profileName":"wifi-null","linkPolicy":null,"ieee8021xProfileName":null,"ieee8021xProfileObject":null}`, + `{"authenticationMethod":"7","linkPolicy":"not-an-array","pskValue":"bad"}`, + fmt.Sprintf(`{"profileName":"huge","authenticationMethod":6,"encryptionMethod":4,"ssid":"ssid","pskPassphrase":"pw","linkPolicy":[%s]}`, intArray(4096)), + fmt.Sprintf(`{"profileName":"nested","authenticationMethod":6,"encryptionMethod":4,"ssid":"ssid","pskPassphrase":"pw","linkPolicy":[1,2],"ieee8021xProfileObject":%s}`, nestedIEEE8021xObject(16)), + `{"profileName":"contradictory","authenticationMethod":7,"encryptionMethod":3,"ssid":"ssid","pskPassphrase":"päss\u0000秘密","linkPolicy":[-1,999999999],"ieee8021xProfileName":null}`, + } + + for _, input := range seedInputs { + f.Add(input) + } + + v := validator.New() + v.SetTagName("binding") + _ = v.RegisterValidation("authforieee8021x", ValidateAuthandIEEE) + _ = v.RegisterValidation("authProtocolValidator", AuthProtocolValidator) + + f.Fuzz(func(t *testing.T, payload string) { + fuzzJSONAndValidate(t, payload, func() *WirelessConfig { return &WirelessConfig{} }, func(value *WirelessConfig) error { + return v.Struct(value) + }) + }) +} + +func FuzzIEEE8021xJSONProcessing(f *testing.F) { + seedInputs := []string{ + `{}`, + `{"profileName":"ieee-1","authenticationProtocol":2,"pxeTimeout":60,"wiredInterface":true}`, + `{"profileName":"nulls","pxeTimeout":null}`, + `{"authenticationProtocol":"2","pxeTimeout":"60","wiredInterface":"false"}`, + fmt.Sprintf(`{"profileName":"nested","authenticationProtocol":2,"pxeTimeout":60,"wiredInterface":true,"junk":%s}`, nestedJSONObject()), + `{"profileName":"edge","authenticationProtocol":999,"pxeTimeout":86401,"wiredInterface":true}`, + } + + for _, input := range seedInputs { + f.Add(input) + } + + v := validator.New() + v.SetTagName("binding") + _ = v.RegisterValidation("authProtocolValidator", AuthProtocolValidator) + + f.Fuzz(func(t *testing.T, payload string) { + fuzzJSONAndValidate(t, payload, func() *IEEE8021xConfig { return &IEEE8021xConfig{} }, func(value *IEEE8021xConfig) error { + return v.Struct(value) + }) + }) +} + +func FuzzProfileWiFiJSONProcessing(f *testing.F) { + seedInputs := []string{ + `{}`, + `{"priority":1,"profileName":"wifi-1","profileProfileName":"profile-1","tenantId":"tenant-1"}`, + `{"priority":null,"profileName":null}`, + `{"priority":"1","profileName":123}`, + fmt.Sprintf(`{"priority":1,"profileName":"wifi-1","profileProfileName":"profile-1","junk":%s}`, nestedJSONObject()), + `{"priority":999999999,"profileName":"wifi_日本🙂","profileProfileName":"profile/特殊","tenantId":"tenant/日本"}`, + } + + for _, input := range seedInputs { + f.Add(input) + } + + v := validator.New() + v.SetTagName("binding") + + f.Fuzz(func(t *testing.T, payload string) { + fuzzJSONAndValidate(t, payload, func() *ProfileWiFiConfigs { return &ProfileWiFiConfigs{} }, func(value *ProfileWiFiConfigs) error { + return v.Struct(value) + }) + }) +} + +func fuzzJSONAndValidate[T any](t *testing.T, payload string, newValue func() *T, validate func(*T) error) { + t.Helper() + + first := newValue() + second := newValue() + + firstErr := json.Unmarshal([]byte(payload), first) + secondErr := json.Unmarshal([]byte(payload), second) + + if (firstErr == nil) != (secondErr == nil) { + t.Fatalf("json.Unmarshal error mismatch for payload %q: first=%v second=%v", payload, firstErr, secondErr) + } + + if firstErr != nil { + if firstErr.Error() != secondErr.Error() { + t.Fatalf("json.Unmarshal error text mismatch for payload %q: first=%q second=%q", payload, firstErr.Error(), secondErr.Error()) + } + + return + } + + if !reflect.DeepEqual(first, second) { + t.Fatalf("json.Unmarshal result mismatch for payload %q", payload) + } + + if validate == nil { + return + } + + firstValidationErr := validate(first) + secondValidationErr := validate(second) + + if (firstValidationErr == nil) != (secondValidationErr == nil) { + t.Fatalf("validation error mismatch for payload %q: first=%v second=%v", payload, firstValidationErr, secondValidationErr) + } + + if firstValidationErr != nil && firstValidationErr.Error() != secondValidationErr.Error() { + t.Fatalf("validation error text mismatch for payload %q: first=%q second=%q", payload, firstValidationErr.Error(), secondValidationErr.Error()) + } +} + +func newProfileValidatorForFuzz() *validator.Validate { + v := validator.New() + v.SetTagName("binding") + _ = v.RegisterValidation("genpasswordwone", ValidateAMTPassOrGenRan) + _ = v.RegisterValidation("ciraortls", ValidateCIRAOrTLS) + _ = v.RegisterValidation("wifidhcp", ValidateWiFiDHCP) + + return v +} + +func quotedArray(value string, count int) string { + items := make([]string, count) + for index := range items { + items[index] = fmt.Sprintf("%q", value) + } + + return strings.Join(items, ",") +} + +func intArray(count int) string { + items := make([]string, count) + for index := range items { + items[index] = fmt.Sprintf("%d", index) + } + + return strings.Join(items, ",") +} + +func repeatedJSON(item string, count int) string { + items := make([]string, count) + for index := range items { + items[index] = item + } + + return strings.Join(items, ",") +} + +const nestedJSONDepth = 32 + +func nestedJSONObject() string { + result := `{"leaf":true}` + for index := 0; index < nestedJSONDepth; index++ { + result = fmt.Sprintf(`{"level%d":%s}`, index, result) + } + + return result +} + +func nestedIEEE8021xObject(depth int) string { + result := `{"profileName":"ieee-1","authenticationProtocol":2,"pxeTimeout":60,"wiredInterface":true}` + for index := 0; index < depth; index++ { + result = fmt.Sprintf(`{"nested%d":%s}`, index, result) + } + + return result +} diff --git a/internal/usecase/ciraconfigs/transform_fuzz_test.go b/internal/usecase/ciraconfigs/transform_fuzz_test.go new file mode 100644 index 000000000..8f4ce21f6 --- /dev/null +++ b/internal/usecase/ciraconfigs/transform_fuzz_test.go @@ -0,0 +1,100 @@ +package ciraconfigs + +import ( + "reflect" + "strings" + "testing" + + "github.com/device-management-toolkit/console/internal/entity" + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/mocks" + "github.com/device-management-toolkit/console/pkg/logger" +) + +func FuzzCIRAConfigTransforms(f *testing.F) { + seedInputs := []struct { + configName string + address string + password string + commonName string + rootCertificate string + proxyDetails string + tenantID string + version string + port int + serverAddressFormat int + authMethod int + generateRandom bool + }{ + {"cira-1", "https://example.com", "P@ssw0rd", "example.com", "root-cert", "http://proxy", "tenant-1", "1.0.0", 4433, 201, 2, false}, + {"", "", "", "", "", "", "", "", 0, 0, 0, false}, + {"cira_日本", "https://例え.テスト", "päss\x00秘密🔐", "例え.テスト", strings.Repeat("r", 2048), "socks5://代理", "tenant/日本", "v1\n2", -1, -4, 99, true}, + {strings.Repeat("c", 2048), strings.Repeat("a", 4096), strings.Repeat("p", 4096), strings.Repeat("n", 1024), strings.Repeat("r", 4096), strings.Repeat("x", 4096), strings.Repeat("t", 1024), strings.Repeat("v", 1024), 65535, 999999, -999999, false}, + } + + for i := range seedInputs { + f.Add(seedInputs[i].configName, seedInputs[i].address, seedInputs[i].password, seedInputs[i].commonName, seedInputs[i].rootCertificate, seedInputs[i].proxyDetails, seedInputs[i].tenantID, seedInputs[i].version, seedInputs[i].port, seedInputs[i].serverAddressFormat, seedInputs[i].authMethod, seedInputs[i].generateRandom) + } + + uc := &UseCase{log: logger.New("error"), safeRequirements: mocks.MockCrypto{}} + + f.Fuzz(func(t *testing.T, configName, address, password, commonName, rootCertificate, proxyDetails, tenantID, version string, port, serverAddressFormat, authMethod int, generateRandom bool) { + buildDTO := func() *dto.CIRAConfig { + return &dto.CIRAConfig{ + ConfigName: configName, + MPSAddress: address, + MPSPort: port, + Username: commonName, + Password: password, + CommonName: commonName, + ServerAddressFormat: serverAddressFormat, + AuthMethod: authMethod, + MPSRootCertificate: rootCertificate, + ProxyDetails: proxyDetails, + TenantID: tenantID, + GenerateRandomPassword: generateRandom, + Version: version, + } + } + + buildEntity := func() *entity.CIRAConfig { + return &entity.CIRAConfig{ + ConfigName: configName, + MPSAddress: address, + MPSPort: port, + Username: commonName, + Password: password, + CommonName: commonName, + ServerAddressFormat: serverAddressFormat, + AuthMethod: authMethod, + MPSRootCertificate: rootCertificate, + ProxyDetails: proxyDetails, + TenantID: tenantID, + GenerateRandomPassword: generateRandom, + Version: version, + } + } + + firstEntity, firstErr := uc.dtoToEntity(buildDTO()) + secondEntity, secondErr := uc.dtoToEntity(buildDTO()) + + if !reflect.DeepEqual(firstErr, secondErr) { + t.Fatalf("dtoToEntity error mismatch") + } + + if firstErr != nil { + t.Fatalf("dtoToEntity returned unexpected error: %v", firstErr) + } + + if !reflect.DeepEqual(firstEntity, secondEntity) { + t.Fatalf("dtoToEntity result mismatch") + } + + firstDTO := uc.entityToDTO(buildEntity()) + secondDTO := uc.entityToDTO(buildEntity()) + + if !reflect.DeepEqual(firstDTO, secondDTO) { + t.Fatalf("entityToDTO result mismatch") + } + }) +} diff --git a/internal/usecase/devices/alarms_fuzz_test.go b/internal/usecase/devices/alarms_fuzz_test.go new file mode 100644 index 000000000..dfe4d2be1 --- /dev/null +++ b/internal/usecase/devices/alarms_fuzz_test.go @@ -0,0 +1,68 @@ +package devices_test + +import ( + "strings" + "testing" + + devices "github.com/device-management-toolkit/console/internal/usecase/devices" +) + +func FuzzParseInterval(f *testing.F) { + seedInputs := []string{ + "", + "P", + "PT", + "P2D", + "PT5H", + "PT30M", + "P1DT6H30M", + "P1DT6H30M45S", + "P0DT0H0M0S", + "PT-1H", + "P-1D", + "PX", + "P1X", + "P1DT", + "PT1Q", + "P999999999D", + "P999999999999999999999999999999D", + "PT999999999999999999999999999999H", + "P1,D", + "P1D/PT2H", + "P1D/../T2H", + "P1D\tT2H", + "P1D\nT2H", + "P1D\x00T2H", + "P日本T2H", + "PT🙂H", + strings.Repeat("P", 128), + strings.Repeat("9", 128), + strings.Repeat("P", 4096), + strings.Repeat("9", 4096) + "D", + } + + for _, input := range seedInputs { + f.Add(input) + } + + f.Fuzz(func(t *testing.T, input string) { + firstResult, firstErr := devices.ParseInterval(input) + secondResult, secondErr := devices.ParseInterval(input) + + if (firstErr == nil) != (secondErr == nil) { + t.Fatalf("ParseInterval error mismatch for %q: first=%v second=%v", input, firstErr, secondErr) + } + + if firstErr != nil { + if firstErr.Error() != secondErr.Error() { + t.Fatalf("ParseInterval error text mismatch for %q: first=%q second=%q", input, firstErr.Error(), secondErr.Error()) + } + + return + } + + if firstResult != secondResult { + t.Fatalf("ParseInterval result mismatch for %q: first=%d second=%d", input, firstResult, secondResult) + } + }) +} diff --git a/internal/usecase/devices/transform_fuzz_test.go b/internal/usecase/devices/transform_fuzz_test.go new file mode 100644 index 000000000..bca9ff933 --- /dev/null +++ b/internal/usecase/devices/transform_fuzz_test.go @@ -0,0 +1,184 @@ +package devices + +import ( + "reflect" + "strings" + "testing" + "time" + + wsmanconfig "github.com/device-management-toolkit/go-wsman-messages/v2/pkg/config" + + "github.com/device-management-toolkit/console/internal/entity" + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/pkg/logger" +) + +const fuzzEncrypted = "encrypted" + +func FuzzDeviceTransforms(f *testing.F) { + seedInputs := []struct { + guid string + dtoTagsRaw string + entityTagsRaw string + password string + mpsPassword string + mebxPassword string + certHash string + tenantID string + username string + useNilTags bool + useTime bool + connectionStatus bool + useTLS bool + allowSelfSigned bool + timestamp int64 + }{ + {"ABC-123", "alpha|beta", "alpha,beta", "P@ssw0rd", "mps-pass", "mebx-pass", "hash", "tenant-1", "admin", false, true, true, true, false, 1700000000}, + {"", "", "", "", "", "", "", "", "", true, false, false, false, false, 0}, + {"UPPER-🙂", "a|\x00|日本|🙂", "a,\x00,日本,🙂", "päss\x00秘密", "пароль", "🔐mebx", "hash/with:special", "tenant/日本", "user\nname", false, true, true, false, true, -2208988800}, + {strings.Repeat("G", 1024), strings.Repeat("tag|", 4096), strings.Repeat("tag,", 4096), strings.Repeat("p", 4096), strings.Repeat("m", 4096), strings.Repeat("x", 8192), strings.Repeat("c", 4096), strings.Repeat("t", 2048), strings.Repeat("u", 1024), false, true, false, true, true, 253402300799}, + } + + for i := range seedInputs { + f.Add(seedInputs[i].guid, seedInputs[i].dtoTagsRaw, seedInputs[i].entityTagsRaw, seedInputs[i].password, seedInputs[i].mpsPassword, seedInputs[i].mebxPassword, seedInputs[i].certHash, seedInputs[i].tenantID, seedInputs[i].username, seedInputs[i].useNilTags, seedInputs[i].useTime, seedInputs[i].connectionStatus, seedInputs[i].useTLS, seedInputs[i].allowSelfSigned, seedInputs[i].timestamp) + } + + uc := &UseCase{log: logger.New("error"), safeRequirements: fuzzCryptorDevice{}} + + f.Fuzz(func(t *testing.T, guid, dtoTagsRaw, entityTagsRaw, password, mpsPassword, mebxPassword, certHash, tenantID, username string, useNilTags, useTime, connectionStatus, useTLS, allowSelfSigned bool, timestamp int64) { + buildTimePtr := func() *time.Time { + if !useTime { + return nil + } + + value := time.Unix(timestamp, 0).UTC() + + return &value + } + + buildDTO := func() *dto.Device { + tags := buildDeviceTags(dtoTagsRaw, useNilTags) + + return &dto.Device{ + ConnectionStatus: connectionStatus, + GUID: guid, + Tags: tags, + TenantID: tenantID, + Username: username, + Password: password, + MPSPassword: mpsPassword, + MEBXPassword: mebxPassword, + UseTLS: useTLS, + AllowSelfSigned: allowSelfSigned, + CertHash: certHash, + LastConnected: buildTimePtr(), + LastSeen: buildTimePtr(), + LastDisconnected: buildTimePtr(), + } + } + + buildEntity := func() *entity.Device { + return &entity.Device{ + ConnectionStatus: connectionStatus, + GUID: guid, + Tags: entityTagsRaw, + TenantID: tenantID, + Username: username, + Password: password, + MPSPassword: stringPtrOrNilDevice(mpsPassword), + MEBXPassword: stringPtrOrNilDevice(mebxPassword), + UseTLS: useTLS, + AllowSelfSigned: allowSelfSigned, + CertHash: stringPtrOrNilDevice(certHash), + LastConnected: buildTimePtr(), + LastSeen: buildTimePtr(), + LastDisconnected: buildTimePtr(), + } + } + + verifyDTOToEntity(t, uc, guid, certHash, buildDTO) + verifyEntityToDTO(t, uc, buildEntity) + }) +} + +func buildDeviceTags(dtoTagsRaw string, useNilTags bool) []string { + if useNilTags { + return nil + } + + if dtoTagsRaw == "" { + return []string{} + } + + return strings.Split(dtoTagsRaw, "|") +} + +func verifyDTOToEntity(t *testing.T, uc *UseCase, guid, certHash string, buildDTO func() *dto.Device) { + t.Helper() + + firstEntity, firstErr := uc.dtoToEntity(buildDTO()) + secondEntity, secondErr := uc.dtoToEntity(buildDTO()) + + if !reflect.DeepEqual(firstErr, secondErr) { + t.Fatalf("dtoToEntity error mismatch") + } + + if firstErr != nil { + t.Fatalf("dtoToEntity returned unexpected error: %v", firstErr) + } + + if !reflect.DeepEqual(firstEntity, secondEntity) { + t.Fatalf("dtoToEntity result mismatch") + } + + if !strings.EqualFold(firstEntity.GUID, guid) { + t.Fatalf("dtoToEntity did not lowercase GUID") + } + + if certHash == "" && firstEntity.CertHash != nil { + t.Fatalf("dtoToEntity expected nil cert hash") + } +} + +func verifyEntityToDTO(t *testing.T, uc *UseCase, buildEntity func() *entity.Device) { + t.Helper() + + firstDTO := uc.entityToDTO(buildEntity()) + secondDTO := uc.entityToDTO(buildEntity()) + + if !reflect.DeepEqual(firstDTO, secondDTO) { + t.Fatalf("entityToDTO result mismatch") + } +} + +func stringPtrOrNilDevice(value string) *string { + if value == "" { + return nil + } + + copyValue := value + + return ©Value +} + +type fuzzCryptorDevice struct{} + +func (fuzzCryptorDevice) Encrypt(string) (string, error) { + return fuzzEncrypted, nil +} + +func (fuzzCryptorDevice) EncryptWithKey(string, string) (string, error) { + return fuzzEncrypted, nil +} + +func (fuzzCryptorDevice) GenerateKey() string { + return "key" +} + +func (fuzzCryptorDevice) Decrypt(string) (string, error) { + return "decrypted", nil +} + +func (fuzzCryptorDevice) ReadAndDecryptFile(string) (wsmanconfig.Configuration, error) { + return wsmanconfig.Configuration{}, nil +} diff --git a/internal/usecase/domains/transform_fuzz_test.go b/internal/usecase/domains/transform_fuzz_test.go new file mode 100644 index 000000000..99fa5c2e2 --- /dev/null +++ b/internal/usecase/domains/transform_fuzz_test.go @@ -0,0 +1,106 @@ +package domains + +import ( + "reflect" + "strings" + "testing" + "time" + + "github.com/device-management-toolkit/console/internal/entity" + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/mocks" + "github.com/device-management-toolkit/console/pkg/logger" +) + +func FuzzDomainTransforms(f *testing.F) { + seedInputs := []struct { + profileName string + domainSuffix string + cert string + password string + expiration string + tenantID string + version string + }{ + {"domain-1", "example.com", "cert", "P@ssw0rd", "2024-01-02T03:04:05Z", "tenant-1", "1.0.0"}, + {"", "", "", "", "", "", ""}, + {"domain_日本", "例え.テスト", strings.Repeat("c", 2048), "päss\x00秘密🔐", "2016-12-31T23:59:60Z", "tenant/日本", "v1\n2"}, + {strings.Repeat("p", 2048), strings.Repeat("d", 2048), strings.Repeat("c", 4096), strings.Repeat("x", 4096), "9999-12-31T23:59:59+14:00", strings.Repeat("t", 2048), strings.Repeat("v", 2048)}, + {"edge-domain", "example.org", "cert", "pw", "0000-01-01T00:00:00Z", "tenant-edge", "2.0.0"}, + {"bad-domain", "example.net", "cert", "pw", "not-a-time", "tenant-bad", "3.0.0"}, + } + + for _, input := range seedInputs { + f.Add(input.profileName, input.domainSuffix, input.cert, input.password, input.expiration, input.tenantID, input.version) + } + + uc := &UseCase{log: logger.New("error"), safeRequirements: mocks.MockCrypto{}} + + f.Fuzz(func(t *testing.T, profileName, domainSuffix, cert, password, expiration, tenantID, version string) { + buildDTO := func() *dto.Domain { + return &dto.Domain{ + ProfileName: profileName, + DomainSuffix: domainSuffix, + ProvisioningCert: cert, + ProvisioningCertPassword: password, + ProvisioningCertStorageFormat: "string", + TenantID: tenantID, + Version: version, + } + } + + buildEntity := func() *entity.Domain { + return &entity.Domain{ + ProfileName: profileName, + DomainSuffix: domainSuffix, + ProvisioningCert: cert, + ProvisioningCertPassword: password, + ProvisioningCertStorageFormat: "string", + ExpirationDate: expiration, + TenantID: tenantID, + Version: version, + } + } + + firstEntity, firstErr := uc.dtoToEntity(buildDTO()) + secondEntity, secondErr := uc.dtoToEntity(buildDTO()) + + if !reflect.DeepEqual(firstErr, secondErr) { + t.Fatalf("dtoToEntity error mismatch") + } + + if firstErr != nil { + t.Fatalf("dtoToEntity returned unexpected error: %v", firstErr) + } + + if !reflect.DeepEqual(firstEntity, secondEntity) { + t.Fatalf("dtoToEntity result mismatch") + } + + verifyDomainEntityToDTO(t, uc, password, expiration, firstEntity, buildEntity) + }) +} + +func verifyDomainEntityToDTO(t *testing.T, uc *UseCase, password, expiration string, firstEntity *entity.Domain, buildEntity func() *entity.Domain) { + t.Helper() + + firstDTO := uc.entityToDTO(buildEntity()) + secondDTO := uc.entityToDTO(buildEntity()) + + if !reflect.DeepEqual(firstDTO, secondDTO) { + t.Fatalf("entityToDTO result mismatch") + } + + expectedTime, err := time.Parse(time.RFC3339, expiration) + if err == nil { + if !firstDTO.ExpirationDate.Equal(expectedTime) { + t.Fatalf("entityToDTO expiration parse mismatch") + } + } else if !firstDTO.ExpirationDate.IsZero() { + t.Fatalf("entityToDTO expected zero expiration for invalid timestamp %q", expiration) + } + + if strings.Contains(password, "\x00") && firstEntity.ProvisioningCertPassword == password { + t.Fatalf("dtoToEntity did not transform password input") + } +} diff --git a/internal/usecase/domains/usecase_fuzz_test.go b/internal/usecase/domains/usecase_fuzz_test.go new file mode 100644 index 000000000..d2b3b8ebd --- /dev/null +++ b/internal/usecase/domains/usecase_fuzz_test.go @@ -0,0 +1,106 @@ +package domains_test + +import ( + "bytes" + "crypto/x509" + "encoding/base64" + "strings" + "testing" + + "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/usecase/domains" +) + +func FuzzDecryptAndCheckCertExpiration(f *testing.F) { + validPFX := generateTestPFX() + expiredPFX := expiredTestPFX() + truncatedValidPFX := validPFX[:len(validPFX)/2] + corruptedValidPFX := corruptBase64String(validPFX) + + seedInputs := []struct { + cert string + password string + }{ + {cert: validPFX, password: "P@ssw0rd"}, + {cert: validPFX, password: "WrongP@ssw0rd"}, + {cert: validPFX, password: "pässwörd"}, + {cert: validPFX, password: "秘密"}, + {cert: validPFX, password: "🔐password"}, + {cert: expiredPFX, password: ""}, + {cert: "", password: ""}, + {cert: "not-base64", password: ""}, + {cert: base64.StdEncoding.EncodeToString([]byte("not a pkcs12 blob")), password: "P@ssw0rd"}, + {cert: truncatedValidPFX, password: "P@ssw0rd"}, + {cert: corruptedValidPFX, password: "P@ssw0rd"}, + {cert: strings.Repeat("A", 256), password: strings.Repeat("B", 32)}, + } + + for _, input := range seedInputs { + f.Add(input.cert, input.password) + } + + f.Fuzz(func(t *testing.T, cert, password string) { + domain := dto.Domain{ + ProvisioningCert: cert, + ProvisioningCertPassword: password, + } + + firstCert, firstErr := domains.DecryptAndCheckCertExpiration(domain) + secondCert, secondErr := domains.DecryptAndCheckCertExpiration(domain) + + if (firstErr == nil) != (secondErr == nil) { + t.Fatalf("DecryptAndCheckCertExpiration error mismatch for cert len=%d password len=%d: first=%v second=%v", len(cert), len(password), firstErr, secondErr) + } + + if firstErr != nil { + verifyDecryptError(t, cert, password, firstErr, secondErr, firstCert, secondCert) + + return + } + + verifyDecryptSuccess(t, cert, password, firstCert, secondCert) + }) +} + +func verifyDecryptError(t *testing.T, cert, password string, firstErr, secondErr error, firstCert, secondCert *x509.Certificate) { + t.Helper() + + if firstErr.Error() != secondErr.Error() { + t.Fatalf("DecryptAndCheckCertExpiration error text mismatch for cert len=%d password len=%d: first=%q second=%q", len(cert), len(password), firstErr.Error(), secondErr.Error()) + } + + if firstCert != nil || secondCert != nil { + t.Fatalf("DecryptAndCheckCertExpiration returned a certificate alongside an error for cert len=%d password len=%d", len(cert), len(password)) + } +} + +func verifyDecryptSuccess(t *testing.T, cert, password string, firstCert, secondCert *x509.Certificate) { + t.Helper() + + if firstCert == nil || secondCert == nil { + t.Fatalf("DecryptAndCheckCertExpiration returned nil certificate without an error for cert len=%d password len=%d", len(cert), len(password)) + + return + } + + if !firstCert.NotAfter.Equal(secondCert.NotAfter) { + t.Fatalf("DecryptAndCheckCertExpiration NotAfter mismatch for cert len=%d password len=%d: first=%s second=%s", len(cert), len(password), firstCert.NotAfter, secondCert.NotAfter) + } + + if !bytes.Equal(firstCert.Raw, secondCert.Raw) { + t.Fatalf("DecryptAndCheckCertExpiration returned different certificate bytes for cert len=%d password len=%d", len(cert), len(password)) + } +} + +func corruptBase64String(input string) string { + decoded, err := base64.StdEncoding.DecodeString(input) + if err != nil || len(decoded) == 0 { + return input + } + + corrupted := append([]byte(nil), decoded...) + index := len(corrupted) / 2 + corrupted[index] ^= 0xff + + return base64.StdEncoding.EncodeToString(corrupted) +} diff --git a/internal/usecase/domains/usecase_test.go b/internal/usecase/domains/usecase_test.go index 2d67231e4..5f5f08c25 100644 --- a/internal/usecase/domains/usecase_test.go +++ b/internal/usecase/domains/usecase_test.go @@ -544,11 +544,15 @@ func TestDecryptAndCheckCertExpiration_Valid(t *testing.T) { assert.NotNil(t, x509Cert) } +func expiredTestPFX() string { + return "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" +} + func TestDecryptAndCheckCertExpiration_Expired(t *testing.T) { t.Parallel() domain := dto.Domain{ - ProvisioningCert: "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", + ProvisioningCert: expiredTestPFX(), ProvisioningCertPassword: "", } diff --git a/internal/usecase/ieee8021xconfigs/transform_fuzz_test.go b/internal/usecase/ieee8021xconfigs/transform_fuzz_test.go new file mode 100644 index 000000000..687a569c1 --- /dev/null +++ b/internal/usecase/ieee8021xconfigs/transform_fuzz_test.go @@ -0,0 +1,81 @@ +package ieee8021xconfigs + +import ( + "reflect" + "strings" + "testing" + + "github.com/device-management-toolkit/console/internal/entity" + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" +) + +func FuzzIEEE8021xConfigTransforms(f *testing.F) { + seedInputs := []struct { + profileName string + tenantID string + version string + authProtocol int + pxeTimeout int + useNilTimeout bool + wired bool + }{ + {"ieee-1", "tenant-1", "1.0.0", 2, 60, false, true}, + {"", "", "", 0, 0, true, false}, + {"ieee_日本", "tenant/日本", "v1\n2", 999999, -1, false, true}, + {strings.Repeat("i", 2048), strings.Repeat("t", 2048), strings.Repeat("v", 1024), -999999, 8640000, false, false}, + } + + for _, input := range seedInputs { + f.Add(input.profileName, input.tenantID, input.version, input.authProtocol, input.pxeTimeout, input.useNilTimeout, input.wired) + } + + uc := &UseCase{} + + f.Fuzz(func(t *testing.T, profileName, tenantID, version string, authProtocol, pxeTimeout int, useNilTimeout, wired bool) { + buildDTO := func() *dto.IEEE8021xConfig { + return &dto.IEEE8021xConfig{ + ProfileName: profileName, + AuthenticationProtocol: authProtocol, + PXETimeout: intPtrIEEE(pxeTimeout, !useNilTimeout), + WiredInterface: wired, + TenantID: tenantID, + Version: version, + } + } + + buildEntity := func() *entity.IEEE8021xConfig { + return &entity.IEEE8021xConfig{ + ProfileName: profileName, + AuthenticationProtocol: authProtocol, + PXETimeout: intPtrIEEE(pxeTimeout, !useNilTimeout), + WiredInterface: wired, + TenantID: tenantID, + Version: version, + } + } + + firstEntity := uc.dtoToEntity(buildDTO()) + secondEntity := uc.dtoToEntity(buildDTO()) + + if !reflect.DeepEqual(firstEntity, secondEntity) { + t.Fatalf("dtoToEntity result mismatch") + } + + firstDTO := uc.entityToDTO(buildEntity()) + secondDTO := uc.entityToDTO(buildEntity()) + + if !reflect.DeepEqual(firstDTO, secondDTO) { + t.Fatalf("entityToDTO result mismatch") + } + }) +} + +func intPtrIEEE(value int, enabled bool) *int { + if !enabled { + return nil + } + + copyValue := value + + return ©Value +} diff --git a/internal/usecase/profiles/transform_fuzz_test.go b/internal/usecase/profiles/transform_fuzz_test.go new file mode 100644 index 000000000..cb2fb492e --- /dev/null +++ b/internal/usecase/profiles/transform_fuzz_test.go @@ -0,0 +1,177 @@ +package profiles + +import ( + "reflect" + "strings" + "testing" + + "github.com/device-management-toolkit/console/internal/entity" + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/mocks" +) + +func FuzzProfileTransforms(f *testing.F) { + seedInputs := []struct { + profileName string + dtoTagsRaw string + entityTags string + amtPassword string + mebxPassword string + ciraName string + ieeeName string + creationDate string + userConsent string + tenantID string + version string + tlsMode int + useNilTags bool + setCIRAPtr bool + setIEEEPtr bool + dhcpEnabled bool + }{ + {"profile-1", "alpha|beta", "alpha,beta", "P@ssw0rd", "MebxP@ss", "cira-1", "ieee-1", "2021-07-01T00:00:00Z", "All", "tenant-1", "1", 1, false, true, true, true}, + {"", "", "", "", "", "", "", "", "", "", "", 0, true, false, false, false}, + {"プロファイル", "a|\x00|日本|🙂", "a,,日本,🙂", "päss\x00秘密", "🔐mēbx", "cira/特殊", "ieee-🙂", "not-a-date", "KVM", "tenant/日本", "v1\n2", 4, false, true, true, false}, + {strings.Repeat("p", 2048), strings.Repeat("tag|", 4096), strings.Repeat("tag,", 4096), strings.Repeat("a", 4096), strings.Repeat("m", 4096), strings.Repeat("c", 1024), strings.Repeat("i", 1024), strings.Repeat("2", 512), "None", strings.Repeat("t", 1024), strings.Repeat("v", 1024), -1, false, true, true, true}, + } + + for i := range seedInputs { + f.Add(seedInputs[i].profileName, seedInputs[i].dtoTagsRaw, seedInputs[i].entityTags, seedInputs[i].amtPassword, seedInputs[i].mebxPassword, seedInputs[i].ciraName, seedInputs[i].ieeeName, seedInputs[i].creationDate, seedInputs[i].userConsent, seedInputs[i].tenantID, seedInputs[i].version, seedInputs[i].tlsMode, seedInputs[i].useNilTags, seedInputs[i].setCIRAPtr, seedInputs[i].setIEEEPtr, seedInputs[i].dhcpEnabled) + } + + uc := &UseCase{safeRequirements: mocks.MockCrypto{}} + + f.Fuzz(func(t *testing.T, profileName, dtoTagsRaw, entityTags, amtPassword, mebxPassword, ciraName, ieeeName, creationDate, userConsent, tenantID, version string, tlsMode int, useNilTags, setCIRAPtr, setIEEEPtr, dhcpEnabled bool) { + buildDTO := func() *dto.Profile { + tags := buildProfileTags(dtoTagsRaw, useNilTags) + + return &dto.Profile{ + ProfileName: profileName, + AMTPassword: amtPassword, + CreationDate: creationDate, + Tags: tags, + DHCPEnabled: dhcpEnabled, + TenantID: tenantID, + TLSMode: tlsMode, + UserConsent: userConsent, + MEBXPassword: mebxPassword, + Version: version, + CIRAConfigName: stringPtrProfile(ciraName, setCIRAPtr), + IEEE8021xProfileName: stringPtrProfile(ieeeName, setIEEEPtr), + } + } + + buildEntity := func() *entity.Profile { + authProtocol := tlsMode + wiredInterface := dhcpEnabled + + return &entity.Profile{ + ProfileName: profileName, + CreationDate: creationDate, + Tags: entityTags, + DHCPEnabled: dhcpEnabled, + TenantID: tenantID, + TLSMode: tlsMode, + UserConsent: userConsent, + GenerateRandomPassword: false, + GenerateRandomMEBxPassword: false, + Version: version, + CIRAConfigName: stringPtrProfile(ciraName, setCIRAPtr), + IEEE8021xProfileName: stringPtrProfile(ieeeName, setIEEEPtr), + AuthenticationProtocol: intPtrProfile(authProtocol, setIEEEPtr && ieeeName != ""), + WiredInterface: boolPtrProfile(wiredInterface, setIEEEPtr && ieeeName != ""), + } + } + + verifyProfileDTOToEntity(t, uc, dtoTagsRaw, useNilTags, buildDTO) + verifyProfileEntityToDTO(t, uc, buildEntity) + }) +} + +func buildProfileTags(dtoTagsRaw string, useNilTags bool) []string { + if useNilTags { + return nil + } + + if dtoTagsRaw == "" { + return []string{} + } + + return strings.Split(dtoTagsRaw, "|") +} + +func verifyProfileDTOToEntity(t *testing.T, uc *UseCase, dtoTagsRaw string, useNilTags bool, buildDTO func() *dto.Profile) { + t.Helper() + + firstEntity, firstErr := uc.dtoToEntity(buildDTO()) + secondEntity, secondErr := uc.dtoToEntity(buildDTO()) + + if !reflect.DeepEqual(firstErr, secondErr) { + t.Fatalf("dtoToEntity error mismatch") + } + + if firstErr != nil { + t.Fatalf("dtoToEntity returned unexpected error: %v", firstErr) + } + + if !reflect.DeepEqual(firstEntity, secondEntity) { + t.Fatalf("dtoToEntity result mismatch") + } + + if firstEntity.Tags != strings.Join(splitTagsProfile(dtoTagsRaw, useNilTags), ", ") { + t.Fatalf("dtoToEntity tag join mismatch") + } +} + +func verifyProfileEntityToDTO(t *testing.T, uc *UseCase, buildEntity func() *entity.Profile) { + t.Helper() + + firstDTO := uc.entityToDTO(buildEntity()) + secondDTO := uc.entityToDTO(buildEntity()) + + if !reflect.DeepEqual(firstDTO, secondDTO) { + t.Fatalf("entityToDTO result mismatch") + } +} + +func splitTagsProfile(raw string, useNilTags bool) []string { + if useNilTags { + return []string{} + } + + if raw == "" { + return []string{} + } + + return strings.Split(raw, "|") +} + +func stringPtrProfile(value string, enabled bool) *string { + if !enabled { + return nil + } + + copyValue := value + + return ©Value +} + +func intPtrProfile(value int, enabled bool) *int { + if !enabled { + return nil + } + + copyValue := value + + return ©Value +} + +func boolPtrProfile(value, enabled bool) *bool { + if !enabled { + return nil + } + + copyValue := value + + return ©Value +} diff --git a/internal/usecase/profilewificonfigs/transform_fuzz_test.go b/internal/usecase/profilewificonfigs/transform_fuzz_test.go new file mode 100644 index 000000000..757d04398 --- /dev/null +++ b/internal/usecase/profilewificonfigs/transform_fuzz_test.go @@ -0,0 +1,64 @@ +package profilewificonfigs + +import ( + "reflect" + "strings" + "testing" + + "github.com/device-management-toolkit/console/internal/entity" + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" +) + +func FuzzProfileWiFiConfigTransforms(f *testing.F) { + seedInputs := []struct { + priority int + wirelessProfileName string + profileName string + tenantID string + }{ + {1, "wireless-1", "profile-1", "tenant-1"}, + {0, "", "", ""}, + {-1, "wireless_日本🙂", "profile/特殊", "tenant/日本"}, + {999999999, strings.Repeat("w", 2048), strings.Repeat("p", 2048), strings.Repeat("t", 2048)}, + } + + for _, input := range seedInputs { + f.Add(input.priority, input.wirelessProfileName, input.profileName, input.tenantID) + } + + uc := &UseCase{} + + f.Fuzz(func(t *testing.T, priority int, wirelessProfileName, profileName, tenantID string) { + buildDTO := func() *dto.ProfileWiFiConfigs { + return &dto.ProfileWiFiConfigs{ + Priority: priority, + WirelessProfileName: wirelessProfileName, + ProfileName: profileName, + TenantID: tenantID, + } + } + + buildEntity := func() *entity.ProfileWiFiConfigs { + return &entity.ProfileWiFiConfigs{ + Priority: priority, + WirelessProfileName: wirelessProfileName, + ProfileName: profileName, + TenantID: tenantID, + } + } + + firstEntity := uc.dtoToEntity(buildDTO()) + secondEntity := uc.dtoToEntity(buildDTO()) + + if !reflect.DeepEqual(firstEntity, secondEntity) { + t.Fatalf("dtoToEntity result mismatch") + } + + firstDTO := uc.entityToDTO(buildEntity()) + secondDTO := uc.entityToDTO(buildEntity()) + + if !reflect.DeepEqual(firstDTO, secondDTO) { + t.Fatalf("entityToDTO result mismatch") + } + }) +} diff --git a/internal/usecase/wificonfigs/transform_fuzz_test.go b/internal/usecase/wificonfigs/transform_fuzz_test.go new file mode 100644 index 000000000..d763c70c8 --- /dev/null +++ b/internal/usecase/wificonfigs/transform_fuzz_test.go @@ -0,0 +1,146 @@ +package wificonfigs + +import ( + "reflect" + "strings" + "testing" + + "github.com/device-management-toolkit/console/internal/entity" + dto "github.com/device-management-toolkit/console/internal/entity/dto/v1" + "github.com/device-management-toolkit/console/internal/mocks" + "github.com/device-management-toolkit/console/pkg/logger" +) + +func FuzzWirelessConfigTransforms(f *testing.F) { + seedInputs := []struct { + profileName string + ssid string + pskPassphrase string + entityLinkPolicy string + ieeeName string + tenantID string + version string + authMethod int + encryptionMethod int + pskValue int + linkA int + linkB int + setIEEEPtr bool + useNilLinkPolicy bool + }{ + {"wifi-1", "ssid", "P@ssw0rd", "1,2,3", "ieee-1", "tenant-1", "1.0.0", 6, 4, 1, 1, 2, true, false}, + {"", "", "", "", "", "", "", 0, 0, 0, 0, 0, false, true}, + {"wifi_日本", "ssid/🙂", "päss\x00秘密", "-1,999999999999,foo,2", "ieee/特殊", "tenant/日本", "v1\n2", 7, 3, -1, -1, 999999999, true, false}, + {strings.Repeat("w", 2048), strings.Repeat("s", 1024), strings.Repeat("p", 4096), strings.Repeat("9,", 4096), strings.Repeat("i", 2048), strings.Repeat("t", 2048), strings.Repeat("v", 1024), -999999, 999999, -999999, -2147483648, 2147483647, false, false}, + } + + for i := range seedInputs { + f.Add(seedInputs[i].profileName, seedInputs[i].ssid, seedInputs[i].pskPassphrase, seedInputs[i].entityLinkPolicy, seedInputs[i].ieeeName, seedInputs[i].tenantID, seedInputs[i].version, seedInputs[i].authMethod, seedInputs[i].encryptionMethod, seedInputs[i].pskValue, seedInputs[i].linkA, seedInputs[i].linkB, seedInputs[i].setIEEEPtr, seedInputs[i].useNilLinkPolicy) + } + + uc := &UseCase{log: logger.New("error"), safeRequirements: mocks.MockCrypto{}} + + f.Fuzz(func(t *testing.T, profileName, ssid, pskPassphrase, entityLinkPolicy, ieeeName, tenantID, version string, authMethod, encryptionMethod, pskValue, linkA, linkB int, setIEEEPtr, useNilLinkPolicy bool) { + buildDTO := func() *dto.WirelessConfig { + return &dto.WirelessConfig{ + ProfileName: profileName, + AuthenticationMethod: authMethod, + EncryptionMethod: encryptionMethod, + SSID: ssid, + PSKValue: pskValue, + PSKPassphrase: pskPassphrase, + LinkPolicy: []int{linkA, linkB}, + TenantID: tenantID, + IEEE8021xProfileName: stringPtrWireless(ieeeName, setIEEEPtr), + Version: version, + } + } + + buildEntity := func() *entity.WirelessConfig { + var linkPolicy *string + if !useNilLinkPolicy { + linkPolicy = stringPtrWireless(entityLinkPolicy, true) + } + + authProtocol := authMethod + wiredInterface := authMethod%2 == 0 + + return &entity.WirelessConfig{ + ProfileName: profileName, + AuthenticationMethod: authMethod, + EncryptionMethod: encryptionMethod, + SSID: ssid, + PSKValue: pskValue, + PSKPassphrase: pskPassphrase, + LinkPolicy: linkPolicy, + TenantID: tenantID, + IEEE8021xProfileName: stringPtrWireless(ieeeName, setIEEEPtr), + Version: version, + AuthenticationProtocol: intPtrWireless(authProtocol, setIEEEPtr && ieeeName != ""), + WiredInterface: boolPtrWireless(wiredInterface, setIEEEPtr && ieeeName != ""), + } + } + + firstEntity, firstErr := uc.dtoToEntity(buildDTO()) + secondEntity, secondErr := uc.dtoToEntity(buildDTO()) + + if !reflect.DeepEqual(firstErr, secondErr) { + t.Fatalf("dtoToEntity error mismatch") + } + + if firstErr != nil { + t.Fatalf("dtoToEntity returned unexpected error: %v", firstErr) + } + + if !reflect.DeepEqual(firstEntity, secondEntity) { + t.Fatalf("dtoToEntity result mismatch") + } + + verifyWifiEntityToDTO(t, uc, useNilLinkPolicy, buildEntity) + }) +} + +func verifyWifiEntityToDTO(t *testing.T, uc *UseCase, useNilLinkPolicy bool, buildEntity func() *entity.WirelessConfig) { + t.Helper() + + firstDTO := uc.entityToDTO(buildEntity()) + secondDTO := uc.entityToDTO(buildEntity()) + + if !reflect.DeepEqual(firstDTO, secondDTO) { + t.Fatalf("entityToDTO result mismatch") + } + + if useNilLinkPolicy && len(firstDTO.LinkPolicy) != 0 { + t.Fatalf("entityToDTO expected empty link policy for nil input") + } +} + +func stringPtrWireless(value string, enabled bool) *string { + if !enabled { + return nil + } + + copyValue := value + + return ©Value +} + +func intPtrWireless(value int, enabled bool) *int { + if !enabled { + return nil + } + + copyValue := value + + return ©Value +} + +func boolPtrWireless(value, enabled bool) *bool { + if !enabled { + return nil + } + + copyValue := value + + return ©Value +}