diff --git a/.gitignore b/.gitignore index 754cb44..7acc985 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ *.dll *.so *.dylib - +bin/* # Test binary matlas matlas-test diff --git a/cmd/config/config.go b/cmd/config/config.go index 177eb50..469846e 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -11,6 +11,7 @@ import ( "gopkg.in/yaml.v3" "github.com/teabranch/matlas-cli/internal/config" + "github.com/teabranch/matlas-cli/internal/fileutil" "github.com/teabranch/matlas-cli/internal/output" "github.com/teabranch/matlas-cli/internal/validation" ) @@ -583,18 +584,15 @@ func runImportConfig(cmd *cobra.Command, sourceFile, targetFile, format string, finalConfig = normalizedConfig } - // Ensure target directory exists - if err := os.MkdirAll(filepath.Dir(targetFile), 0o750); err != nil { - return fmt.Errorf("failed to create target directory: %w", err) - } - - // Convert to YAML and write to target file + // Convert to YAML outputData, err := yaml.Marshal(finalConfig) if err != nil { return fmt.Errorf("failed to marshal configuration: %w", err) } - if err := os.WriteFile(targetFile, outputData, 0o600); err != nil { + // SECURITY: Write file with secure permissions + writer := fileutil.NewSecureFileWriter() + if err := writer.WriteFile(targetFile, outputData); err != nil { return fmt.Errorf("failed to write target file: %w", err) } @@ -642,7 +640,9 @@ func runExportConfig(cmd *cobra.Command, outputFile, format string, includeSecre // Write to file or stdout if outputFile != "" { - if err := os.WriteFile(outputFile, output, 0o600); err != nil { + // SECURITY: Write file with secure permissions + writer := fileutil.NewSecureFileWriter() + if err := writer.WriteFile(outputFile, output); err != nil { return fmt.Errorf("failed to write to output file: %w", err) } fmt.Printf("āœ… Configuration exported successfully to: %s\n", outputFile) @@ -703,7 +703,9 @@ func runMigrateConfig(cmd *cobra.Command, fromVersion, toVersion string, backup // Create backup if requested if backup { backupFile := configFile + ".backup." + strings.ReplaceAll(fromVersion, ".", "_") - if err := os.WriteFile(backupFile, configData, 0o600); err != nil { + // SECURITY: Write backup with secure permissions + writer := fileutil.NewSecureFileWriter() + if err := writer.WriteFile(backupFile, configData); err != nil { return fmt.Errorf("failed to create backup: %w", err) } fmt.Printf("šŸ“ Backup created: %s\n", backupFile) @@ -721,7 +723,9 @@ func runMigrateConfig(cmd *cobra.Command, fromVersion, toVersion string, backup return fmt.Errorf("failed to marshal migrated configuration: %w", err) } - if err := os.WriteFile(configFile, migratedData, 0o600); err != nil { + // SECURITY: Write file with secure permissions + writer := fileutil.NewSecureFileWriter() + if err := writer.WriteFile(configFile, migratedData); err != nil { return fmt.Errorf("failed to write migrated configuration: %w", err) } diff --git a/cmd/root.go b/cmd/root.go index a6b3f43..3f80e2c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -47,6 +47,17 @@ var ( Long: "matlas-cli enables unified management of MongoDB Atlas resources and standalone MongoDB databases.", SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // SECURITY: Check if credentials provided via insecure flags + if cmd.Flags().Changed("api-key") || cmd.Flags().Changed("pub-key") { + return fmt.Errorf( + "ERROR: Passing credentials via command-line flags is insecure.\n" + + "Command-line arguments are visible in process listings and shell history.\n\n" + + "Please use one of these secure methods instead:\n" + + " 1. Environment variables: ATLAS_API_KEY and ATLAS_PUB_KEY\n" + + " 2. Config file: ~/.matlas/config.yaml\n" + + " 3. Platform keychain (see documentation)") + } + // 1. Initialize enhanced logging logConfig := &logging.Config{ Level: logging.LevelInfo, diff --git a/docs/alerts.md b/docs/alerts.md index d3618d8..65cad67 100644 --- a/docs/alerts.md +++ b/docs/alerts.md @@ -536,7 +536,7 @@ See the [Alert Examples]({{ '/examples/alerts/' | relative_url }}) for comprehen - [Alert Examples]({{ '/examples/alerts/' | relative_url }}) - Working YAML examples - [Atlas Commands]({{ '/atlas/#alerts' | relative_url }}) - CLI command reference -- [YAML Kinds Reference]({{ '/yaml-kinds/#alertconfiguration' | relative_url }}) - Complete AlertConfiguration reference +- [YAML Kinds Reference]({{ '/reference/#alertconfiguration' | relative_url }}) - Complete AlertConfiguration reference - [Infrastructure Commands]({{ '/infra/' | relative_url }}) - Apply and manage configurations --- diff --git a/docs/atlas.md b/docs/atlas.md index 07fc649..7308e2c 100644 --- a/docs/atlas.md +++ b/docs/atlas.md @@ -239,7 +239,7 @@ matlas atlas clusters update my-cluster --project-id --pit - **Point-in-Time Recovery** (`--pit`): Recovery to any specific moment in time (requires backup) - **Cross-Region Backup**: Use multi-region cluster configurations (see YAML examples) -**Note:** For complex cluster configurations with multi-region setups, use [infrastructure workflows](/infra/) with YAML configurations. +**Note:** For complex cluster configurations with multi-region setups, use [infrastructure workflows]({{ '/infra/' | relative_url }}) with YAML configurations. ## Atlas Search diff --git a/docs/dag-engine.md b/docs/dag-engine.md index eabd09f..37fe836 100644 --- a/docs/dag-engine.md +++ b/docs/dag-engine.md @@ -794,6 +794,6 @@ For very large configurations (1000+ operations): ## Further Reading -- [Infrastructure Workflows](/infra/) - General infrastructure management -- [Discovery Documentation](/discovery/) - Enumerating Atlas resources -- [YAML Kinds Reference](/yaml-kinds-reference/) - Configuration format details +- [Infrastructure Workflows]({{ '/infra/' | relative_url }}) - General infrastructure management +- [Discovery Documentation]({{ '/discovery/' | relative_url }}) - Enumerating Atlas resources +- [YAML Kinds Reference]({{ '/reference/yaml-kinds/' | relative_url }}) - Configuration format details diff --git a/docs/database.md b/docs/database.md index 3d56f96..7d46e58 100644 --- a/docs/database.md +++ b/docs/database.md @@ -386,7 +386,7 @@ All database user management is handled via `matlas atlas users` commands. Users ### User Management via Atlas API -For complete user management documentation, see the [Atlas Commands](/atlas/) documentation. Here are the essential commands: +For complete user management documentation, see the [Atlas Commands]({{ '/atlas/' | relative_url }}) documentation. Here are the essential commands: ```bash # Create Atlas database user (propagates to MongoDB databases) diff --git a/docs/examples.md b/docs/examples.md index 34815f0..df0ca9c 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -79,11 +79,11 @@ Complete infrastructure management workflows - Safe operations with preserve-existing - Dependency management -### [Search & VPC]({{ '/examples/advanced/' | relative_url }}) -Advanced Atlas features -- Atlas Search index configurations -- VPC endpoint setups -- Vector search for AI applications +### [DAG Analysis]({{ '/examples/dag-analysis/' | relative_url }}) +Infrastructure optimization and analysis +- Dependency graph visualization +- Critical path analysis +- Bottleneck detection and optimization suggestions ### [Alerts & Monitoring]({{ '/examples/alerts/' | relative_url }}) Atlas alert configurations for monitoring and notifications @@ -145,7 +145,7 @@ matlas infra apply -f current.yaml --preserve-existing ## Related Documentation -- [YAML Kinds Reference]({{ '/yaml-kinds/' | relative_url }}) - Complete reference for all resource types +- [YAML Kinds Reference]({{ '/reference/' | relative_url }}) - Complete reference for all resource types - [Infrastructure Commands]({{ '/infra/' | relative_url }}) - `plan`, `apply`, `diff`, and `destroy` operations - [Atlas Commands]({{ '/atlas/' | relative_url }}) - Direct Atlas resource management - [Database Commands]({{ '/database/' | relative_url }}) - MongoDB database operations diff --git a/docs/examples/alerts.md b/docs/examples/alerts.md index f37f59f..dfd82d4 100644 --- a/docs/examples/alerts.md +++ b/docs/examples/alerts.md @@ -461,7 +461,7 @@ notifications: ## Related Documentation - [Alert CLI Commands]({{ '/atlas/#alerts' | relative_url }}) - Command-line alert management -- [YAML Kinds Reference]({{ '/yaml-kinds/#alertconfiguration' | relative_url }}) - Complete AlertConfiguration reference +- [YAML Kinds Reference]({{ '/reference/#alertconfiguration' | relative_url }}) - Complete AlertConfiguration reference - [Infrastructure Commands]({{ '/infra/' | relative_url }}) - Apply and manage alert configurations - [Atlas Documentation]({{ '/atlas/' | relative_url }}) - Atlas resource management diff --git a/docs/examples/clusters.md b/docs/examples/clusters.md index cc2af44..97274a2 100644 --- a/docs/examples/clusters.md +++ b/docs/examples/clusters.md @@ -4,6 +4,7 @@ title: Cluster Examples parent: Examples nav_order: 2 description: MongoDB cluster configurations for different environments +permalink: /examples/clusters/ --- # Cluster Examples diff --git a/docs/examples/dag-analysis.md b/docs/examples/dag-analysis.md index 5957bcf..754df3c 100644 --- a/docs/examples/dag-analysis.md +++ b/docs/examples/dag-analysis.md @@ -3,6 +3,8 @@ layout: default title: DAG Analysis Examples parent: Examples nav_order: 8 +description: DAG engine examples for analyzing and visualizing infrastructure deployments +permalink: /examples/dag-analysis/ --- # DAG Analysis Examples @@ -707,6 +709,6 @@ jq -r '.criticalPathDuration' metrics/analysis-*.json ## Further Reading -- [DAG Engine Documentation](/dag-engine/) - Complete feature guide -- [Infrastructure Workflows](/infra/) - Plan, diff, apply workflows -- [Discovery Documentation](/discovery/) - Enumerating Atlas resources +- [DAG Engine Documentation]({{ '/dag-engine/' | relative_url }}) - Complete feature guide +- [Infrastructure Workflows]({{ '/infra/' | relative_url }}) - Plan, diff, apply workflows +- [Discovery Documentation]({{ '/discovery/' | relative_url }}) - Enumerating Atlas resources diff --git a/docs/examples/discovery.md b/docs/examples/discovery.md index 37543c0..704413e 100644 --- a/docs/examples/discovery.md +++ b/docs/examples/discovery.md @@ -4,6 +4,7 @@ title: Discovery Examples parent: Examples nav_order: 1 description: Convert existing Atlas resources to infrastructure-as-code format +permalink: /examples/discovery/ --- # Discovery Examples diff --git a/docs/examples/infrastructure.md b/docs/examples/infrastructure.md index ce79490..c2add23 100644 --- a/docs/examples/infrastructure.md +++ b/docs/examples/infrastructure.md @@ -4,6 +4,7 @@ title: Infrastructure Patterns parent: Examples nav_order: 6 description: Complete infrastructure management workflows and patterns +permalink: /examples/infrastructure/ --- # Infrastructure Patterns diff --git a/docs/examples/network.md b/docs/examples/network.md index 1538d6a..c843f0e 100644 --- a/docs/examples/network.md +++ b/docs/examples/network.md @@ -4,6 +4,7 @@ title: Network Access parent: Examples nav_order: 5 description: IP allowlisting and network security configurations +permalink: /examples/network/ --- # Network Access Examples @@ -288,4 +289,4 @@ ipAddress: "54.123.45.67" - [Clusters]({{ '/examples/clusters/' | relative_url }}) - Cluster configurations needing network access - [Infrastructure Patterns]({{ '/examples/infrastructure/' | relative_url }}) - Complete infrastructure with network security -- [VPC Endpoints]({{ '/examples/advanced/' | relative_url }}) - Private network connectivity \ No newline at end of file +- [YAML Kinds Reference]({{ '/reference/yaml-kinds/' | relative_url }}) - VPC endpoint configuration reference \ No newline at end of file diff --git a/docs/examples/roles.md b/docs/examples/roles.md index 5874c82..b910b90 100644 --- a/docs/examples/roles.md +++ b/docs/examples/roles.md @@ -4,6 +4,7 @@ title: Custom Roles parent: Examples nav_order: 4 description: Granular permission management with custom database roles +permalink: /examples/roles/ --- # Custom Roles Examples diff --git a/docs/examples/users.md b/docs/examples/users.md index 3c3d936..f162e14 100644 --- a/docs/examples/users.md +++ b/docs/examples/users.md @@ -4,6 +4,7 @@ title: User Management parent: Examples nav_order: 3 description: Database user and authentication examples +permalink: /examples/users/ --- # User Management Examples diff --git a/docs/infra.md b/docs/infra.md index d8ee829..56590fb 100644 --- a/docs/infra.md +++ b/docs/infra.md @@ -19,7 +19,7 @@ Terraform-inspired workflows for managing MongoDB Atlas infrastructure as code. Matlas provides infrastructure-as-code workflows inspired by Terraform and kubectl: -1. **[Discover](/discovery/)** → Enumerate current Atlas resources and optionally include database-level resources +1. **[Discover]({{ '/discovery/' | relative_url }})** → Enumerate current Atlas resources and optionally include database-level resources 2. **Plan/Diff** → Preview changes before applying 3. **Apply** → Reconcile desired state 4. **Show** → Display current state @@ -40,7 +40,7 @@ The discovery feature supports comprehensive resource enumeration including clus Enumerate Atlas resources for a project and optionally convert to ApplyDocument format. -**For comprehensive discovery documentation, see [Discovery](/discovery/)** +**For comprehensive discovery documentation, see [Discovery]({{ '/discovery/' | relative_url }})** ### Basic discovery ```bash @@ -76,7 +76,7 @@ matlas discover \ - **Database discovery**: Include databases, collections, indexes, and custom roles - **Authentication flexibility**: Temporary users, manual credentials, or direct connection strings -See [Discovery documentation](/discovery/) for complete usage guide and examples. +See [Discovery documentation]({{ '/discovery/' | relative_url }}) for complete usage guide and examples. --- @@ -105,7 +105,7 @@ matlas infra optimize -f config.yaml --project-id - Generate visual dependency graphs - Get actionable optimization recommendations -**For comprehensive documentation, see [DAG Engine](/dag-engine/)** +**For comprehensive documentation, see [DAG Engine]({{ '/dag-engine/' | relative_url }})** --- diff --git a/docs/yaml-kinds.md b/docs/yaml-kinds.md index dc86f68..cf9cd95 100644 --- a/docs/yaml-kinds.md +++ b/docs/yaml-kinds.md @@ -595,10 +595,10 @@ spec: ## Related Documentation -- {{ '/infra/' | relative_url }} - Infrastructure commands (`apply`, `plan`, `diff`) -- {{ '/atlas/' | relative_url }} - Atlas resource management -- {{ '/database/' | relative_url }} - Database operations -- {{ '/auth/' | relative_url }} - Authentication and configuration +- [Infrastructure Commands]({{ '/infra/' | relative_url }}) - Infrastructure commands (`apply`, `plan`, `diff`) +- [Atlas Commands]({{ '/atlas/' | relative_url }}) - Atlas resource management +- [Database Commands]({{ '/database/' | relative_url }}) - Database operations +- [Authentication]({{ '/auth/' | relative_url }}) - Authentication and configuration - [Examples]({{ '/examples/' | relative_url }}) - Working examples for all kinds --- diff --git a/internal/clients/mongodb/client.go b/internal/clients/mongodb/client.go index dbe2e7a..3b3dce0 100644 --- a/internal/clients/mongodb/client.go +++ b/internal/clients/mongodb/client.go @@ -4,7 +4,10 @@ package mongodb import ( "context" "crypto/tls" + "flag" "fmt" + "os" + "strings" "time" "go.mongodb.org/mongo-driver/bson" @@ -13,6 +16,7 @@ import ( "go.mongodb.org/mongo-driver/mongo/readpref" "github.com/teabranch/matlas-cli/internal/logging" + "github.com/teabranch/matlas-cli/internal/security" "github.com/teabranch/matlas-cli/internal/types" ) @@ -53,6 +57,42 @@ func DefaultClientConfig() *ClientConfig { } } +// ValidateTLSConfig validates TLS configuration and enforces security requirements +func (c *ClientConfig) ValidateTLSConfig() error { + if c.TLSInsecure && c.TLSEnabled { + // Check for explicit override environment variable + if os.Getenv("MATLAS_ALLOW_INSECURE_TLS") != "true" { + // Check if we're in a test environment + if !isTestEnvironment() { + return fmt.Errorf( + "TLS certificate verification is disabled. " + + "This is INSECURE and only allowed for testing. " + + "Set MATLAS_ALLOW_INSECURE_TLS=true to override (NOT recommended for production)") + } + } + + // Log warning even if allowed + logging.Default().Warn("TLS certificate verification is DISABLED - this is insecure!") + } + return nil +} + +// isTestEnvironment checks if we're running in a test context +func isTestEnvironment() bool { + // Check if running under go test + if flag.Lookup("test.v") != nil { + return true + } + // Check if binary name suggests test execution + if len(os.Args) > 0 { + arg0 := os.Args[0] + if strings.Contains(arg0, ".test") || strings.Contains(arg0, "/_test/") { + return true + } + } + return false +} + // NewClient creates a new MongoDB client with Atlas-optimized settings func NewClient(ctx context.Context, config *ClientConfig, logger *logging.Logger) (*Client, error) { if config == nil { @@ -63,6 +103,11 @@ func NewClient(ctx context.Context, config *ClientConfig, logger *logging.Logger logger = logging.Default() } + // SECURITY: Validate TLS configuration before proceeding + if err := config.ValidateTLSConfig(); err != nil { + return nil, err + } + clientOptions := options.Client(). ApplyURI(config.ConnectionString). SetConnectTimeout(config.ConnectTimeout). @@ -106,7 +151,7 @@ func NewClient(ctx context.Context, config *ClientConfig, logger *logging.Logger } logger.Info("Successfully connected to MongoDB", - "connection_string", maskConnectionString(config.ConnectionString)) + "connection_string", security.MaskConnectionString(config.ConnectionString)) return &Client{ client: client, @@ -426,12 +471,3 @@ func (c *Client) ListIndexes(ctx context.Context, dbName, collectionName string) return indexes, nil } - -// maskConnectionString masks sensitive information in connection strings for logging -func maskConnectionString(connectionString string) string { - // Simple masking - in production, use more sophisticated parsing - if len(connectionString) > 50 { - return connectionString[:20] + "***MASKED***" + connectionString[len(connectionString)-10:] - } - return "***MASKED***" -} diff --git a/internal/config/credentials.go b/internal/config/credentials.go index 79d84db..f657415 100644 --- a/internal/config/credentials.go +++ b/internal/config/credentials.go @@ -98,6 +98,23 @@ func (c *Config) CreateAtlasClient() (*atlasclient.Client, error) { // getCredentialFromPlatformStore retrieves credentials from platform-specific secure storage func getCredentialFromPlatformStore(service string) string { + // SECURITY: Strict allowlist validation to prevent command injection + allowedServices := map[string]bool{ + "api-key": true, + "pub-key": true, + } + + if !allowedServices[service] { + return "" + } + + // Additional validation: only lowercase letters and hyphens allowed + for _, r := range service { + if !((r >= 'a' && r <= 'z') || r == '-') { + return "" + } + } + switch runtime.GOOS { case "darwin": return getCredentialFromMacOSKeychain(service) diff --git a/internal/fileutil/secure_writer.go b/internal/fileutil/secure_writer.go new file mode 100644 index 0000000..d963d39 --- /dev/null +++ b/internal/fileutil/secure_writer.go @@ -0,0 +1,83 @@ +// Package fileutil provides secure file operation utilities +package fileutil + +import ( + "fmt" + "os" + "path/filepath" +) + +// SecureFileWriter handles secure file operations with appropriate permissions +type SecureFileWriter struct { + fileMode os.FileMode + dirMode os.FileMode +} + +// NewSecureFileWriter creates a writer for sensitive files with restrictive permissions +func NewSecureFileWriter() *SecureFileWriter { + return &SecureFileWriter{ + fileMode: 0600, // rw------- (owner read/write only) + dirMode: 0700, // rwx------ (owner full access only) + } +} + +// WriteFile securely writes data to a file with proper permissions +func (w *SecureFileWriter) WriteFile(path string, data []byte) error { + // Create parent directory with secure permissions + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, w.dirMode); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + // Write file with secure permissions + if err := os.WriteFile(path, data, w.fileMode); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + // Verify permissions (defense in depth) + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("failed to verify file permissions: %w", err) + } + + if info.Mode().Perm() != w.fileMode { + // Attempt to fix permissions + if err := os.Chmod(path, w.fileMode); err != nil { + return fmt.Errorf("failed to set secure permissions: %w", err) + } + } + + return nil +} + +// WriteFileWithMode writes a file with custom permissions +func (w *SecureFileWriter) WriteFileWithMode(path string, data []byte, mode os.FileMode) error { + // Create parent directory with secure permissions + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, w.dirMode); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + // Write file with specified permissions + if err := os.WriteFile(path, data, mode); err != nil { + return fmt.Errorf("failed to write file: %w", err) + } + + return nil +} + +// EnsureSecurePermissions ensures existing file has secure permissions +func (w *SecureFileWriter) EnsureSecurePermissions(path string) error { + info, err := os.Stat(path) + if err != nil { + return fmt.Errorf("failed to stat file: %w", err) + } + + if info.Mode().Perm() != w.fileMode { + if err := os.Chmod(path, w.fileMode); err != nil { + return fmt.Errorf("failed to set secure permissions: %w", err) + } + } + + return nil +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go index dbca725..baa419a 100644 --- a/internal/logging/logger.go +++ b/internal/logging/logger.go @@ -7,10 +7,22 @@ import ( "io" "log/slog" "os" + "regexp" "strings" "time" ) +// Pre-compiled regex patterns for secret detection (compiled once at package init) +// These patterns are used in the hot path (WithFields logging), so pre-compilation +// avoids repeated regex compilation overhead on every log call. +var ( + secretPatternJWT = regexp.MustCompile(`^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.[A-Za-z0-9-_.+/=]+$`) + secretPatternBase64Key = regexp.MustCompile(`^[A-Za-z0-9+/]{32,}={0,2}$`) + secretPatternMongoDBURI = regexp.MustCompile(`^mongodb(\+srv)?://.*@`) + secretPatternAWSKey = regexp.MustCompile(`^AKIA[0-9A-Z]{16}$`) + secretPatternHexKey = regexp.MustCompile(`^[a-fA-F0-9]{32,}$`) +) + // LogLevel represents different log levels. type LogLevel int @@ -179,8 +191,8 @@ func (l *Logger) Critical(msg string, args ...any) { func (l *Logger) WithFields(fields map[string]any) *Logger { args := make([]any, 0, len(fields)*2) for k, v := range fields { - // Mask secrets if enabled - if l.config.MaskSecrets && l.isSecret(k) { + // SECURITY: Mask if key suggests secret OR value looks like secret + if l.config.MaskSecrets && (l.isSecret(k) || l.containsSecretValue(v)) { v = l.maskValue(v) } args = append(args, k, v) @@ -349,21 +361,63 @@ func (l *Logger) LogMetric(name string, value float64, unit string, tags map[str // Helper methods for secret masking func (l *Logger) isSecret(key string) bool { - secretKeys := []string{ + // Expanded keyword list for comprehensive secret detection + secretKeywords := []string{ "api_key", "apikey", "api-key", - "password", "passwd", "pwd", - "token", "auth", "authorization", - "secret", "private_key", "private-key", - "connection_string", "connection-string", - "mongodb_uri", "mongo_uri", + "password", "passwd", "pwd", "pass", + "token", "auth", "authorization", "bearer", + "secret", "private_key", "private-key", "privatekey", + "connection_string", "connection-string", "connectionstring", + "mongodb_uri", "mongo_uri", "uri", + "credential", "creds", + "access_key", "accesskey", "access-key", + "session", "cookie", + "certificate", "cert", "key", + "public_key", "publickey", "public-key", } lowerKey := strings.ToLower(key) - for _, secretKey := range secretKeys { - if strings.Contains(lowerKey, secretKey) { + for _, keyword := range secretKeywords { + if strings.Contains(lowerKey, keyword) { return true } } + + return false +} + +// containsSecretValue performs pattern-based secret detection on values +// Uses pre-compiled regex patterns (defined at package level) for performance +// since this method is called frequently in the logging hot path. +func (l *Logger) containsSecretValue(value interface{}) bool { + str, ok := value.(string) + if !ok { + return false + } + + // Minimum length check - very short strings are unlikely to be secrets + if len(str) < 8 { + return false + } + + // Check against pre-compiled secret patterns + // Pattern matching is done in order of likelihood for early exit optimization + if secretPatternMongoDBURI.MatchString(str) { + return true + } + if secretPatternJWT.MatchString(str) { + return true + } + if secretPatternBase64Key.MatchString(str) { + return true + } + if secretPatternAWSKey.MatchString(str) { + return true + } + if secretPatternHexKey.MatchString(str) { + return true + } + return false } diff --git a/internal/output/create_formatters.go b/internal/output/create_formatters.go index 095d4eb..a9c7109 100644 --- a/internal/output/create_formatters.go +++ b/internal/output/create_formatters.go @@ -4,12 +4,14 @@ package output import ( "fmt" "io" + "os" "reflect" "strings" "text/tabwriter" "time" "github.com/teabranch/matlas-cli/internal/config" + "github.com/teabranch/matlas-cli/internal/security" ) // CreateResultFormatter provides pretty formatting for create command results @@ -176,7 +178,7 @@ func (f *CreateResultFormatter) formatClusterCreateResult(result interface{}) er return err } for name, uri := range connectionStrings { - if _, err := fmt.Fprintf(w, " %s:\t%s\n", name, maskConnectionString(uri)); err != nil { + if _, err := fmt.Fprintf(w, " %s:\t%s\n", name, security.MaskConnectionString(uri)); err != nil { return err } } @@ -216,10 +218,21 @@ func (f *CreateResultFormatter) formatUserCreateResultWithPassword(result interf } } - // Show password if provided + // SECURITY: Only show password if explicitly requested via environment variable if password != "" { - if _, err := fmt.Fprintf(w, "Password:\t%s\n", password); err != nil { - return err + showPassword := os.Getenv("MATLAS_SHOW_PASSWORD") == "true" + + if showPassword { + if _, err := fmt.Fprintf(w, "Password:\t%s\n", password); err != nil { + return err + } + if _, err := fmt.Fprintf(w, "\nāš ļø WARNING:\tPassword displayed in plaintext. Clear your terminal history!\n"); err != nil { + return err + } + } else { + if _, err := fmt.Fprintf(w, "Password:\t[HIDDEN - Set MATLAS_SHOW_PASSWORD=true to display]\n"); err != nil { + return err + } } } @@ -245,11 +258,11 @@ func (f *CreateResultFormatter) formatUserCreateResultWithPassword(result interf } // Show appropriate tip based on password display - if password != "" { - if _, err := fmt.Fprintf(w, "\nāš ļø Warning:\tPassword displayed above for convenience. Store it securely!\n"); err != nil { + if password != "" && os.Getenv("MATLAS_SHOW_PASSWORD") != "true" { + if _, err := fmt.Fprintf(w, "\nšŸ’” Tip:\tPassword hidden for security. Set MATLAS_SHOW_PASSWORD=true to display it\n"); err != nil { return err } - } else { + } else if password == "" { if _, err := fmt.Fprintf(w, "\nšŸ’” Tip:\tUser password is not shown for security reasons\n"); err != nil { return err } @@ -517,44 +530,6 @@ func getConnectionStrings(v reflect.Value) map[string]string { return result } -// maskConnectionString obfuscates credentials within a MongoDB connection string. -// Examples: -// - mongodb+srv://user:pass@host/db -> mongodb+srv://user:***@host/db -// - mongodb://user:p%40ss@host/?x=y -> mongodb://user:***@host/?x=y -// -// If no credentials are present, returns the input unchanged. -func maskConnectionString(uri string) string { - // Fast path: if there is no '@' there cannot be embedded credentials - if !strings.Contains(uri, "@") { - return uri - } - // Split scheme and the rest - parts := strings.SplitN(uri, "://", 2) - if len(parts) != 2 { - return uri - } - scheme, rest := parts[0], parts[1] - - // Find credentials segment before '@' - atIdx := strings.Index(rest, "@") - if atIdx == -1 { - return uri - } - - credsAndHost := rest[:atIdx] // everything before '@' - // creds may be in the form user or user:password - colonIdx := strings.Index(credsAndHost, ":") - if colonIdx == -1 { - // No password provided, nothing to mask - return uri - } - user := credsAndHost[:colonIdx] - // Replace password with *** - maskedCreds := user + ":***" - masked := scheme + "://" + maskedCreds + rest[atIdx:] - return masked -} - func getRoles(v reflect.Value) []string { var roles []string if rolesField := v.FieldByName("Roles"); rolesField.IsValid() { diff --git a/internal/output/formatters_extended_test.go b/internal/output/formatters_extended_test.go index c7f4795..5ea7e46 100644 --- a/internal/output/formatters_extended_test.go +++ b/internal/output/formatters_extended_test.go @@ -11,6 +11,7 @@ import ( "gopkg.in/yaml.v3" "github.com/teabranch/matlas-cli/internal/config" + "github.com/teabranch/matlas-cli/internal/security" ) func TestFormatter_FormatJSON(t *testing.T) { @@ -411,12 +412,12 @@ func TestMaskConnectionString(t *testing.T) { { name: "srv with creds", in: "mongodb+srv://user:pass@cluster.mongodb.net/db?retryWrites=true&w=majority", - want: "mongodb+srv://user:***@cluster.mongodb.net/db?retryWrites=true&w=majority", + want: "mongodb+srv://user@cluster.mongodb.net/db?retryWrites=true&w=majority", }, { name: "standard with encoded password", in: "mongodb://alice:p%40ss@localhost:27017/?ssl=true", - want: "mongodb://alice:***@localhost:27017/?ssl=true", + want: "mongodb://alice@localhost:27017/?ssl=true", }, { name: "no creds", @@ -432,19 +433,23 @@ func TestMaskConnectionString(t *testing.T) { for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - got := maskConnectionString(tt.in) + got := security.MaskConnectionString(tt.in) if got != tt.want { t.Fatalf("maskConnectionString() = %q, want %q", got, tt.want) } - // Ensure masking occurs only when userinfo contains a password + // Ensure password is not present in output parts := strings.SplitN(tt.in, "://", 2) if len(parts) == 2 { rest := parts[1] if at := strings.Index(rest, "@"); at != -1 { userinfo := rest[:at] if strings.Contains(userinfo, ":") { - if !strings.Contains(got, ":***@") { - t.Fatalf("expected masked credentials in output: %q", got) + // Extract password from original + passwordStart := strings.Index(userinfo, ":") + password := userinfo[passwordStart+1:] + // Ensure password is not in masked output + if strings.Contains(got, password) { + t.Fatalf("password leaked in masked output: %q contains %q", got, password) } } } diff --git a/internal/security/masking.go b/internal/security/masking.go new file mode 100644 index 0000000..d42cdfc --- /dev/null +++ b/internal/security/masking.go @@ -0,0 +1,84 @@ +// Package security provides security utilities for masking sensitive information +package security + +import ( + "net/url" + "strings" +) + +// MaskConnectionString safely masks credentials in MongoDB connection strings +// while preserving the username and structure for debugging purposes. +// +// Examples: +// - mongodb://user:pass@host/db -> mongodb://user:***@host/db +// - mongodb+srv://admin:P@ssw0rd!@cluster.mongodb.net/test -> mongodb+srv://admin:***@cluster.mongodb.net/test +// - mongodb://host/db (no credentials) -> mongodb://host/db (unchanged) +func MaskConnectionString(uri string) string { + if uri == "" { + return "" + } + + // Fast path: if no '@', there are no credentials + if !strings.Contains(uri, "@") { + return uri + } + + // Parse as URL to safely extract components + parsedURL, err := url.Parse(uri) + if err != nil { + // Fallback to simple masking if parsing fails + return "***MASKED_URI***" + } + + // If there's user info, mask the password + if parsedURL.User != nil { + username := parsedURL.User.Username() + if username != "" { + // Keep username visible but mask password completely + parsedURL.User = url.User(username) + } else { + // No username, clear user info entirely + parsedURL.User = nil + } + } + + return parsedURL.String() +} + +// MaskCredentialInString masks any credential-like strings in text +// by showing only the first and last 4 characters. +// +// This is useful for API keys, tokens, and other credential types. +// +// Examples: +// - "abcdefghijklmnopqrst" -> "abcd************qrst" +// - "short" -> "***" +func MaskCredentialInString(text string) string { + if text == "" { + return "" + } + + // For very short strings, mask completely + if len(text) <= 8 { + return "***" + } + + // Show first 4 and last 4 characters + return text[:4] + strings.Repeat("*", len(text)-8) + text[len(text)-4:] +} + +// MaskValue provides intelligent masking based on value type +func MaskValue(value interface{}) interface{} { + str, ok := value.(string) + if !ok { + return "***" + } + + // Check if it looks like a connection string + if strings.HasPrefix(str, "mongodb://") || strings.HasPrefix(str, "mongodb+srv://") { + return MaskConnectionString(str) + } + + // Default to credential masking + return MaskCredentialInString(str) +} diff --git a/internal/services/database/temp_user.go b/internal/services/database/temp_user.go index abb1746..c9f9819 100644 --- a/internal/services/database/temp_user.go +++ b/internal/services/database/temp_user.go @@ -61,7 +61,11 @@ func (m *TempUserManager) CreateTempUser(ctx context.Context, config TempUserCon // Respect a caller-supplied password if provided, otherwise generate one. password := config.Password if password == "" { - password = generateSecurePassword() + var err error + password, err = generateSecurePassword() + if err != nil { + return nil, fmt.Errorf("failed to generate secure password: %w", err) + } } // Create Atlas SDK user object @@ -286,23 +290,27 @@ func generateTempUsername(purpose string) string { return fmt.Sprintf("matlas-%s-%d-%s", purpose, timestamp, randomStr) } -func generateSecurePassword() string { +func generateSecurePassword() (string, error) { // Generate a secure random password using URL-safe characters // Avoiding special characters that could cause URL encoding issues const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - password := make([]byte, 32) - - for i := range password { - randomBytes := make([]byte, 1) - if _, err := rand.Read(randomBytes); err != nil { - // On failure, deterministically fall back to 'a' - password[i] = 'a' - continue - } - password[i] = charset[int(randomBytes[0])%len(charset)] + const passwordLength = 32 + + password := make([]byte, passwordLength) + randomBytes := make([]byte, passwordLength) + + // SECURITY: Read all random bytes at once (more efficient) + if _, err := rand.Read(randomBytes); err != nil { + // FAIL FAST - never generate weak passwords + return "", fmt.Errorf("failed to generate secure random password: %w", err) + } + + // Convert random bytes to charset + for i, b := range randomBytes { + password[i] = charset[int(b)%len(charset)] } - return string(password) + return string(password), nil } func (m *TempUserManager) isTempUser(user admin.CloudDatabaseUser) bool { diff --git a/tracking/documentation.md b/tracking/documentation.md index 8a71836..c4542bd 100644 --- a/tracking/documentation.md +++ b/tracking/documentation.md @@ -1,198 +1,58 @@ # Documentation Tracking -This file tracks documentation updates, README changes, and content improvements. +## [2025-12-11] Documentation Link Fixes -## Template for New Entries - -```markdown -## [YYYY-MM-DD] [Brief Description] - -**Status**: [In Progress | Completed | Cancelled] -**Developer**: [Name/Username] -**Related Issues**: [#123, #456] - -### Summary -Brief description of the documentation work. - -### Tasks -- [x] Task 1 description -- [x] Task 2 description -- [ ] Task 3 description - -### Files Modified -- `path/to/file1.md` - Description of changes -- `path/to/file2.md` - Description of changes - -### Notes -Any important decisions, blockers, or context for future developers. - ---- -``` - ---- - -## [2025-01-27] Release Process Documentation Update - -**Status**: Completed -**Developer**: Assistant -**Related Issues**: User feedback about outdated release documentation +**Status**: Completed +**Developer**: Assistant +**Related Issues**: User reported broken documentation links ### Summary -Updated release process documentation to accurately reflect the consolidated workflow implementation after the move from separate workflows (ci.yml, semantic-release.yml, release.yml) to a single consolidated release.yml workflow. +Fixed all broken and incorrect links in the documentation site to resolve Jekyll routing issues and 404 errors. ### Tasks -- [x] Analyze differences between documented process and actual workflow implementation -- [x] Update workflow description to reflect consolidated approach -- [x] Correct automatic release process documentation -- [x] Update manual release process documentation -- [x] Fix troubleshooting section for single workflow architecture -- [x] Update manual override instructions -- [x] Add changelog entry for documentation correction +- [x] Add missing permalinks to all example pages +- [x] Fix broken links to non-existent /examples/advanced/ page +- [x] Fix raw links missing Jekyll relative_url filter +- [x] Fix incorrect yaml-kinds permalink references +- [x] Verify all documentation links are correct ### Files Modified -- `docs/release-process.md` - Complete rewrite of workflow section to reflect consolidated release.yml implementation -- `CHANGELOG.md` - Added entry in Unreleased section for documentation correction -### Notes -The original documentation described a multi-workflow approach: -1. Separate CI workflow (ci.yml) for building artifacts -2. Separate semantic-release workflow (semantic-release.yml) -3. Separate release workflow (release.yml) for attaching assets - -However, the actual implementation uses a single consolidated workflow (`release.yml`) that includes: -1. Code quality & linting job -2. Cross-platform testing job (Ubuntu, macOS, Windows) -3. Multi-platform build job (Linux, macOS, Windows for multiple architectures) -4. Checksum generation job -5. Semantic release job (main branch only) with artifacts attached -6. Conditional integration & E2E testing jobs - -The documentation now accurately reflects this consolidated approach and provides correct troubleshooting guidance for the single-workflow architecture. - ---- - -## [2025-01-27] Development Guide Creation - -**Status**: Completed -**Developer**: Assistant -**Related Issues**: User request for development documentation - -### Summary -Created comprehensive development guide that explains how to use the workspace cursor rules and tracking systems for feature development. This includes task tracking, feature interface consistency, service layer architecture, and documentation standards. - -### Tasks -- [x] Explore existing docs structure and navigation -- [x] Create comprehensive development guide explaining cursor rules and tracking -- [x] Update docs navigation to include the new development guide -- [x] Update permanent tracking with this documentation work - -### Files Modified -- `docs/development.md` - New comprehensive development guide covering workspace rules, tracking systems, and development workflow -- `docs/_config.yml` - Added "Development Guide" to navigation menu +#### Added Permalinks (7 files) +- `docs/examples/clusters.md` - Added permalink: /examples/clusters/ +- `docs/examples/discovery.md` - Added permalink: /examples/discovery/ +- `docs/examples/users.md` - Added permalink: /examples/users/ +- `docs/examples/roles.md` - Added permalink: /examples/roles/ +- `docs/examples/network.md` - Added permalink: /examples/network/ +- `docs/examples/infrastructure.md` - Added permalink: /examples/infrastructure/ +- `docs/examples/dag-analysis.md` - Added permalink: /examples/dag-analysis/ + +#### Fixed Broken Links (2 files) +- `docs/examples.md` - Changed "Search & VPC" to link to DAG Analysis +- `docs/examples/network.md` - Changed VPC Endpoints to link to YAML Kinds Reference + +#### Fixed Raw Links (5 files) +- `docs/infra.md` - Fixed 4 links missing relative_url filter +- `docs/dag-engine.md` - Fixed 3 links in Further Reading +- `docs/atlas.md` - Fixed 1 link to /infra/ +- `docs/database.md` - Fixed 1 link to /atlas/ +- `docs/examples/dag-analysis.md` - Fixed links in Further Reading + +#### Fixed Permalink Paths (3 files) +- `docs/alerts.md` - Updated /yaml-kinds/ to /reference/ +- `docs/examples/alerts.md` - Updated /yaml-kinds/ to /reference/ +- `docs/yaml-kinds.md` - Fixed malformed Related Documentation links + +### Commits +1. `2493b5e` - docs: add missing permalinks to example pages +2. `e22a563` - docs: fix broken links to non-existent /examples/advanced/ page +3. `a44fbc5` - docs: fix raw links missing Jekyll relative_url filter +4. `054daf5` - docs: fix incorrect yaml-kinds permalink references ### Notes -The development guide provides a complete reference for developers on: -- Task tracking system (both session-level todos and permanent tracking) -- Feature interface consistency requirements (CLI + YAML ApplyDocument) -- Service layer architecture patterns -- Documentation standards following Jekyll/GitHub Pages setup -- Changelog and release management with Conventional Commits -- Code quality standards and acceptance checklists - -This ensures all developers follow the established workspace rules and maintain consistency across the codebase. +- Main issue: Example pages were accessible at incorrect URLs (e.g., /examples/clusters.html vs /examples/clusters/) +- Root cause: Missing `permalink` frontmatter in Jekyll pages +- Secondary issues: Links using wrong paths and missing relative_url filters +- All changes pushed to `fix/security-patches` branch --- - -## [2025-01-27] Database User Management Documentation Correction - -**Status**: Completed -**Developer**: Assistant -**Related Issues**: User feedback about incorrect documentation - -### Summary -Corrected misleading documentation in `docs/database.md` that incorrectly claimed there were two different types of user management (Atlas vs Database). Fixed to reflect actual implementation where all users are created via Atlas API and propagate to MongoDB databases. - -### Tasks -- [x] Correct main distinction section between Atlas and Database commands -- [x] Remove false separation between "Atlas users" and "Database users" -- [x] Rewrite Database Users section to clarify Atlas-managed nature -- [x] Update examples to show correct Atlas user creation patterns -- [x] Add clarifying comments in YAML examples - -### Files Modified -- `docs/database.md` - Major revision to Database Users section and command distinction explanation - -### Notes -The original documentation incorrectly suggested that `matlas database users` commands would create users directly in MongoDB using `createUser` commands. However, the actual implementation shows: - -1. All user management goes through Atlas API (`internal/services/atlas/users.go`) -2. The `cmd/database/users/users.go` commands are stubs that redirect to Atlas commands -3. Tests in `database-operations.sh` correctly use `matlas atlas users create` -4. Users created via Atlas automatically propagate to MongoDB databases - -This correction eliminates confusion and aligns documentation with the actual codebase behavior. The user management model is: **Atlas API → User Creation → Propagation to MongoDB Databases**. - ---- - -## [2025-01-28] Alert System Documentation Update - -**Status**: Completed -**Developer**: Assistant -**Related Issues**: User request to update docs with new alert functionality - -### Summary -Comprehensive documentation update for the new MongoDB Atlas alerting system, including YAML kinds documentation, CLI command documentation, usage guides, and examples. Added complete alert management and configuration capabilities to the documentation suite. - -### Tasks -- [x] Update YAML kinds documentation to include AlertConfiguration and Alert kinds -- [x] Add comprehensive alert CLI commands documentation to atlas.md -- [x] Create dedicated alerts.md documentation page with complete usage guide -- [x] Update examples documentation to reference alert examples -- [x] Create dedicated alert examples documentation page -- [x] Update main index page to feature alerts functionality -- [x] Add permanent tracking entry for documentation work - -### Files Modified -- `docs/yaml-kinds-reference.md` - Added AlertConfiguration and Alert kinds to supported kinds table and detailed kind documentation -- `docs/yaml-kinds.md` - Added comprehensive AlertConfiguration and Alert kind documentation with examples -- `docs/atlas.md` - Added complete alerts section with CLI commands, YAML configuration, and feature documentation -- `docs/examples.md` - Added alerts & monitoring section to examples categories -- `docs/examples/alerts.md` - New comprehensive alert examples documentation with usage patterns -- `docs/alerts.md` - New dedicated alerts documentation page with complete usage guide -- `docs/index.md` - Added alerts & monitoring feature card and updated examples list -- `tracking/documentation.md` - This entry documenting the alert documentation work - -### Notes -The alert system documentation covers: - -**YAML Kinds Documentation:** -- AlertConfiguration kind for creating and managing alert rules -- Alert kind for read-only alert status monitoring -- Complete field reference and examples for both kinds - -**CLI Commands Documentation:** -- Alert management commands (list, get, acknowledge) -- Alert configuration management commands (list, get, delete, matcher-fields) -- Complete command examples with all flags and options - -**Usage Documentation:** -- Comprehensive alerts.md page covering all aspects of alert usage -- Event types, notification channels, matchers, and thresholds -- Best practices, troubleshooting, and debugging guidance -- Integration with infrastructure-as-code workflows - -**Examples Documentation:** -- Dedicated alert examples page with working YAML configurations -- Basic CPU monitoring, multi-channel notifications, comprehensive monitoring setups -- Advanced matcher and threshold patterns -- Environment-specific and escalation patterns - -**Feature Integration:** -- Updated main documentation pages to reference alert functionality -- Integrated alerts into the overall CLI feature set -- Cross-referenced with related documentation sections - -This documentation update provides complete coverage of the alert system functionality, enabling users to effectively monitor their MongoDB Atlas infrastructure with comprehensive alerting capabilities. - ---- -