Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ jobs:
go test -run=^$ -fuzz=^FuzzDeactivateURL$ -fuzztime=${FUZZ_TIME} ./internal/cli
go test -run=^$ -fuzz=^FuzzDeactivatePassword$ -fuzztime=${FUZZ_TIME} ./internal/cli
go test -run=^$ -fuzz=^FuzzDeactivateFlagCombinations$ -fuzztime=${FUZZ_TIME} ./internal/cli
go test -run=^$ -fuzz=^FuzzActivate$ -fuzztime=${FUZZ_TIME} ./internal/cli
go test -run=^$ -fuzz=^FuzzActivateURL$ -fuzztime=${FUZZ_TIME} ./internal/cli
go test -run=^$ -fuzz=^FuzzActivateProfile$ -fuzztime=${FUZZ_TIME} ./internal/cli
go test -run=^$ -fuzz=^FuzzActivatePassword$ -fuzztime=${FUZZ_TIME} ./internal/cli
go test -run=^$ -fuzz=^FuzzActivateFlagCombinations$ -fuzztime=${FUZZ_TIME} ./internal/cli

- name: Upload fuzz failure artifacts
if: failure()
Expand Down Expand Up @@ -100,6 +105,11 @@ jobs:
go test ./internal/cli -run=^$ -fuzz=^FuzzDeactivateURL$ -fuzztime=1x
go test ./internal/cli -run=^$ -fuzz=^FuzzDeactivatePassword$ -fuzztime=1x
go test ./internal/cli -run=^$ -fuzz=^FuzzDeactivateFlagCombinations$ -fuzztime=1x
go test ./internal/cli -run=^$ -fuzz=^FuzzActivate$ -fuzztime=1x
go test ./internal/cli -run=^$ -fuzz=^FuzzActivateURL$ -fuzztime=1x
go test ./internal/cli -run=^$ -fuzz=^FuzzActivateProfile$ -fuzztime=1x
go test ./internal/cli -run=^$ -fuzz=^FuzzActivatePassword$ -fuzztime=1x
go test ./internal/cli -run=^$ -fuzz=^FuzzActivateFlagCombinations$ -fuzztime=1x

- name: Report regression test results
if: failure()
Expand Down
313 changes: 313 additions & 0 deletions internal/cli/cli_fuzz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,3 +273,316 @@ func FuzzDeactivateFlagCombinations(f *testing.F) {
_ = err
})
}

// FuzzActivate tests the activate command with various flag combinations and inputs
func FuzzActivate(f *testing.F) {
// Seed corpus with valid and invalid activate command patterns
seeds := []string{
// Local activation (CCM)
"--local --ccm",
"--ccm",
"--ccm --password admin",
"--local --ccm --password Passw0rd!",

// Local activation (ACM, provisioning cert path)
"--local --acm",
"--acm",
"--acm --password admin --provisioningCert MIIabc== --provisioningCertPwd certpass",
"--acm --password admin --provisioningCert MIIabc== --provisioningCertPwd certpass --mebxpassword Mebx123!",
"--acm --tls-tunnel --password admin",

// Stop configuration
"--local --stopConfig",
"--stopConfig",

// Legacy remote activation (ws/wss)
"--url wss://server.com --profile profile1",
"--url ws://server.com:8080/path --profile profile1",
"--url wss://server.com --profile p1 --proxy http://proxy.corp:8080",
"--url wss://server.com --profile p1 --tenantid tenant-1",

// HTTP profile activation (fullflow) with auth
"--url https://server.com/api/v1/admin/profiles/export/default",
"--url http://localhost:8080/profiles/export/default --key 12345678901234567890123456789012",
"--url https://server.com/api/v1/admin/profiles/export/p1 --auth-token abc.def.ghi",
"--url https://server.com/api/v1/admin/profiles/export/p1 --auth-username user --auth-password secret",
"--url https://server.com/profiles/export/p1 --auth-endpoint https://server.com/api/v1/authorize --auth-token abc",
"--url https://server.com/profiles/export/p1 --devices-endpoint https://server.com/api/v1/devices --auth-token abc",

// Local profile file activation
"--profile profile.yaml",
"--profile ./configs/profile.yml --key 12345678901234567890123456789012",

// Optional fields
"--local --ccm --dns corp.example.com --hostname host-1 --name my-device",
"--local --ccm --skipIPRenew",
"--url wss://server.com --profile p1 --uuid 123e4567-e89b-12d3-a456-426614174000",

// Invalid combinations (should fail validation)
"--local",
"--local --ccm --acm",
"--url wss://server.com",
"--url wss://server.com --profile p1 --local",
"--url wss://server.com --profile p1 --ccm",
"--url wss://server.com --profile p1 --provisioningCert MIIabc==",
"--url wss://server.com --profile p1 --skipIPRenew",
"--url https://server.com/export/p1 --ccm",
"--auth-username user",
"--profile profile-name",
"",

// Edge cases
"--url " + strings.Repeat("https://a", 40),
"--profile " + strings.Repeat("a", 500),
"--password " + strings.Repeat("a", 500),
"--local --ccm --name \"device with spaces\"",
"--url https://user:pass@host:9999/path?query=value",
}

for _, seed := range seeds {
f.Add(seed)
}

f.Fuzz(func(t *testing.T, flags string) {
if len(flags) > 10000 {
t.Skip("Input too long")
}

ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockAMTCommand := setupMockAMT(ctrl)

args := []string{"rpc", "activate"}
if trimmed := strings.TrimSpace(flags); trimmed != "" {
args = append(args, strings.Fields(flags)...)
}
Comment on lines +356 to +359

defer recoverPanic(t, flags)

_, _, err := Parse(args, mockAMTCommand)
_ = err
Comment on lines +361 to +364
})
}

// FuzzActivateURL tests URL parsing and validation for activate command
func FuzzActivateURL(f *testing.F) {
seeds := []string{
"https://localhost",
"https://server.com",
"https://server.com:443",
"https://server.com:8080/path",
"wss://websocket.server.com",
"ws://websocket.server.com",
"http://insecure.server.com",
"://missing-scheme",
"ftp://wrong-protocol.com",
"https://",
"server.com",
strings.Repeat("https://", 100),
"https://" + strings.Repeat("a", 2000),
"https://server.com/path?query=value#fragment",
"https://server.com/path with spaces",
}

for _, seed := range seeds {
f.Add(seed)
}

f.Fuzz(func(t *testing.T, url string) {
if len(url) > 5000 {
t.Skip("URL too long")
}

ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockAMTCommand := setupMockAMT(ctrl)

args := []string{"rpc", "activate", "--url", url, "--profile", "default"}
defer recoverPanic(t, url)

_, _, err := Parse(args, mockAMTCommand)
_ = err
Comment on lines +403 to +406
})
}

// FuzzActivateProfile tests profile input handling for activate command
func FuzzActivateProfile(f *testing.F) {
seeds := []string{
"default",
"profile-1",
"profile.yaml",
"./profiles/default.yml",
"../profiles/edge.case.json",
"",
"profile with spaces.yaml",
"path/with/special_-.chars.yaml",
strings.Repeat("a", 1000),
"パスファイル.yaml",
}

for _, seed := range seeds {
f.Add(seed)
}

f.Fuzz(func(t *testing.T, profile string) {
if len(profile) > 5000 {
t.Skip("Profile too long")
}

ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockAMTCommand := setupMockAMT(ctrl)

args := []string{"rpc", "activate", "--profile", profile}
defer recoverPanic(t, profile)

_, _, err := Parse(args, mockAMTCommand)
_ = err
Comment on lines +440 to +443
})
}

// FuzzActivatePassword tests AMT password input handling for activate command.
// The AMT password is set during local CCM/ACM activation, so malformed or
// unusual passwords must be parsed without panicking.
func FuzzActivatePassword(f *testing.F) {
seeds := []string{
"admin",
"Password123!",
"",
"pass with spaces",
"pass\"with\"quotes",
"pass'with'quotes",
"pass\\with\\escapes",
"パスワード", // Unicode
"🔒🔑", // Emoji
strings.Repeat("a", 1000),
"$pecial@Ch@rs!",
"$(command)",
"`backticks`",
"; echo test",
}

for _, seed := range seeds {
f.Add(seed)
}

f.Fuzz(func(t *testing.T, password string) {
if len(password) > 5000 {
t.Skip("Password too long")
}

ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockAMTCommand := setupMockAMT(ctrl)

// Local CCM activation is the primary path that consumes --password.
args := []string{"rpc", "activate", "--local", "--ccm", "--password", password}
defer recoverPanic(t, password)

_, _, err := Parse(args, mockAMTCommand)
_ = err
Comment on lines +484 to +487
})
}

// FuzzActivateFlagCombinations tests various combinations of activate flags
func FuzzActivateFlagCombinations(f *testing.F) {
f.Fuzz(func(t *testing.T,
local bool,
ccm bool,
acm bool,
stopConfig bool,
tlsTunnel bool,
skipIPRenew bool,
url string,
profile string,
key string,
password string,
provisioningCert string,
mebxPassword string,
authToken string,
jsonOutput bool,
verbose bool,
) {
if len(url) > 1000 || len(profile) > 1000 || len(key) > 1000 ||
len(password) > 1000 || len(provisioningCert) > 1000 ||
len(mebxPassword) > 1000 || len(authToken) > 1000 {
t.Skip("Input too long")
}

ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockAMTCommand := setupMockAMT(ctrl)

args := []string{"rpc", "activate"}

if local {
args = append(args, "--local")
}

if ccm {
args = append(args, "--ccm")
}

if acm {
args = append(args, "--acm")
}

if stopConfig {
args = append(args, "--stopConfig")
}

if tlsTunnel {
args = append(args, "--tls-tunnel")
}

if skipIPRenew {
args = append(args, "--skipIPRenew")
}

if url != "" {
args = append(args, "--url", url)
}

if profile != "" {
args = append(args, "--profile", profile)
}

if key != "" {
args = append(args, "--key", key)
}

if password != "" {
args = append(args, "--password", password)
}

if provisioningCert != "" {
args = append(args, "--provisioningCert", provisioningCert)
}

if mebxPassword != "" {
args = append(args, "--mebxpassword", mebxPassword)
}

if authToken != "" {
args = append(args, "--auth-token", authToken)
}

if jsonOutput {
args = append(args, "--json")
}

if verbose {
args = append(args, "--verbose")
}

defer recoverPanic(t, strings.Join(args, " "))

_, _, err := Parse(args, mockAMTCommand)
_ = err
Comment on lines +583 to +586
})
}
17 changes: 16 additions & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,32 @@ fuzz: ### run fuzz tests for extended duration (5 minutes per test)
go test -run=^$$ -fuzz=^FuzzDeactivateURL$$ -fuzztime=5m ./internal/cli
go test -run=^$$ -fuzz=^FuzzDeactivatePassword$$ -fuzztime=5m ./internal/cli
go test -run=^$$ -fuzz=^FuzzDeactivateFlagCombinations$$ -fuzztime=5m ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivate$$ -fuzztime=5m ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivateURL$$ -fuzztime=5m ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivateProfile$$ -fuzztime=5m ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivatePassword$$ -fuzztime=5m ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivateFlagCombinations$$ -fuzztime=5m ./internal/cli

fuzz-short: ### run fuzz tests for short duration (30 seconds per test)
@echo "Running quick fuzz tests for 30 seconds each..."
go test -run=^$$ -fuzz=^FuzzDeactivate$$ -fuzztime=30s ./internal/cli
go test -run=^$$ -fuzz=^FuzzDeactivateURL$$ -fuzztime=30s ./internal/cli
go test -run=^$$ -fuzz=^FuzzDeactivatePassword$$ -fuzztime=30s ./internal/cli
go test -run=^$$ -fuzz=^FuzzDeactivateFlagCombinations$$ -fuzztime=30s ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivate$$ -fuzztime=30s ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivateURL$$ -fuzztime=30s ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivateProfile$$ -fuzztime=30s ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivatePassword$$ -fuzztime=30s ./internal/cli
go test -run=^$$ -fuzz=^FuzzActivateFlagCombinations$$ -fuzztime=30s ./internal/cli

fuzz-regression: ### run fuzz tests with existing corpus only (no new inputs)
@echo "Running fuzz regression tests..."
go test ./internal/cli -run=^$$ -fuzz=^FuzzDeactivate$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzDeactivateURL$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzDeactivatePassword$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzDeactivateFlagCombinations$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzDeactivateFlagCombinations$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzActivate$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzActivateURL$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzActivateProfile$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzActivatePassword$$ -fuzztime=1x
go test ./internal/cli -run=^$$ -fuzz=^FuzzActivateFlagCombinations$$ -fuzztime=1x
Loading