package snmpagent import ( "context" "errors" "os" "path/filepath" "runtime" "strings" "testing" ) type recordingRunner struct { actions []string fail bool } func (r *recordingRunner) Run(_ context.Context, _ string, args ...string) ([]byte, error) { r.actions = append(r.actions, strings.Join(args, " ")) if r.fail && len(r.actions) == 1 { return []byte("restart failed"), errors.New("exit status 1") } return nil, nil } func TestOSControllerRendersVersionedCommunitiesAndV3Users(t *testing.T) { directory := t.TempDir() mainPath := filepath.Join(directory, "etc", "snmpd.conf") persistentPath := filepath.Join(directory, "state", "snmpd.conf") runner := &recordingRunner{} controller, err := NewOSController(OSOptions{ ConfigPath: mainPath, PersistentPath: persistentPath, InitScriptPath: filepath.Join(directory, "S59snmpd"), Runner: runner, }) if err != nil { t.Fatal(err) } config := Config{ Enabled: true, Location: `Rack "A"`, Contact: "noc@example.local", V1Enabled: true, V2CEnabled: true, V3Enabled: true, ReadOnlyCommunity: "read-only-secret", ReadWriteCommunity: "write-secret", V3Users: []User{{ Username: "snmp-admin", Access: "write", AuthProtocol: "sha256", PrivacyProtocol: "aes", AuthKey: "auth-secret-123", PrivacyKey: "privacy-secret-123", Enabled: true, }}, } if err := controller.Apply(context.Background(), config); err != nil { t.Fatal(err) } mainConfig, err := os.ReadFile(mainPath) if err != nil { t.Fatal(err) } persistentConfig, err := os.ReadFile(persistentPath) if err != nil { t.Fatal(err) } mainText := string(mainConfig) for _, expected := range []string{ "agentAddress udp:161", `sysLocation "Rack \"A\""`, "group tera_ro_v1 v1 tera_ro", "group tera_ro_v2c v2c tera_ro", "access tera_ro_v1 \"\" any noauth exact tera_all none none", "access tera_rw_v2c \"\" any noauth exact tera_all tera_all none", "rwuser snmp-admin priv .1", } { if !strings.Contains(mainText, expected) { t.Fatalf("main configuration missing %q:\n%s", expected, mainText) } } if strings.Contains(mainText, "auth-secret") || strings.Contains(mainText, "privacy-secret") { t.Fatalf("SNMPv3 credentials leaked into main configuration: %s", mainText) } if !strings.Contains(string(persistentConfig), `createUser snmp-admin SHA-256 "auth-secret-123" AES "privacy-secret-123"`) { t.Fatalf("persistent configuration = %s", persistentConfig) } if len(runner.actions) != 1 || runner.actions[0] != "restart" { t.Fatalf("service actions = %v", runner.actions) } if runtime.GOOS != "windows" { for _, path := range []string{mainPath, persistentPath} { info, err := os.Stat(path) if err != nil || info.Mode().Perm() != 0o600 { t.Fatalf("%s mode = %v, err=%v", path, info.Mode().Perm(), err) } } } } func TestOSControllerRestoresFilesWhenRestartFails(t *testing.T) { directory := t.TempDir() mainPath := filepath.Join(directory, "snmpd.conf") persistentPath := filepath.Join(directory, "persistent.conf") if err := os.WriteFile(mainPath, []byte("agentAddress udp:161\nold-main\n"), 0o600); err != nil { t.Fatal(err) } if err := os.WriteFile(persistentPath, []byte("old-persistent\n"), 0o600); err != nil { t.Fatal(err) } runner := &recordingRunner{fail: true} controller, err := NewOSController(OSOptions{ ConfigPath: mainPath, PersistentPath: persistentPath, InitScriptPath: filepath.Join(directory, "S59snmpd"), Runner: runner, }) if err != nil { t.Fatal(err) } config := Config{Enabled: true, V2CEnabled: true, ReadOnlyCommunity: "community-secret"} if err := controller.Apply(context.Background(), config); err == nil || !strings.Contains(err.Error(), "restart failed") { t.Fatalf("apply error = %v", err) } mainConfig, _ := os.ReadFile(mainPath) persistentConfig, _ := os.ReadFile(persistentPath) if string(mainConfig) != "agentAddress udp:161\nold-main\n" || string(persistentConfig) != "old-persistent\n" { t.Fatalf("rollback files = %q, %q", mainConfig, persistentConfig) } if len(runner.actions) != 2 || runner.actions[1] != "restart" { t.Fatalf("rollback actions = %v", runner.actions) } } func TestValidateRejectsConfigInjection(t *testing.T) { config := Config{Enabled: true, V3Enabled: true, V3Users: []User{{ Username: "bad user", Access: "read", AuthProtocol: "none", PrivacyProtocol: "none", Enabled: true, }}} if err := Validate(config); err == nil { t.Fatal("unsafe SNMPv3 username accepted") } config = Config{Enabled: true, V2CEnabled: true, ReadOnlyCommunity: "bad\ncommunity"} if err := Validate(config); err == nil { t.Fatal("control character in community accepted") } }