Critical fixes: - Add SanitizeConnectionString() in storage/db_connect.go to remove passwords - Add SecureEnvVar() in api/factory.go to clear env vars after reading (JWT_SECRET) - Clear DB password from config after connection Logging improvements: - Enhance logging/sanitize.go with patterns for: - PostgreSQL connection strings - Generic connection string passwords - HTTP Authorization headers - Private keys CLI security: - Add --security-audit flag to api-server for security checks: - Config file permissions - Exposed environment variables - Running as root - API key file permissions - Add warning when --api-key flag used (process list exposure) Files changed: - internal/storage/db_connect.go - internal/api/factory.go - internal/logging/sanitize.go - internal/auth/flags.go - cmd/api-server/main.go
164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
// Package storage provides database abstraction and job management.
|
|
package storage
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "github.com/lib/pq" // PostgreSQL driver
|
|
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
|
)
|
|
|
|
// SanitizeConnectionString removes passwords from connection strings for safe logging
|
|
func SanitizeConnectionString(connStr string) string {
|
|
if connStr == "" {
|
|
return ""
|
|
}
|
|
|
|
// Pattern for postgresql://user:pass@host format
|
|
postgresPattern := regexp.MustCompile(`(postgresql?://[^:]+:)[^@]+(@)`)
|
|
sanitized := postgresPattern.ReplaceAllString(connStr, "${1}[REDACTED]${2}")
|
|
|
|
// Pattern for password=xxxxx in connection string
|
|
passwordPattern := regexp.MustCompile(`(password=)[^\s]*`)
|
|
sanitized = passwordPattern.ReplaceAllString(sanitized, "${1}[REDACTED]")
|
|
|
|
return sanitized
|
|
}
|
|
|
|
// DBConfig holds database connection configuration.
|
|
type DBConfig struct {
|
|
Type string
|
|
Connection string
|
|
Host string
|
|
Port int
|
|
Username string
|
|
Password string
|
|
Database string
|
|
}
|
|
|
|
// DB wraps a database connection with type information.
|
|
type DB struct {
|
|
conn *sql.DB
|
|
dbType string
|
|
}
|
|
|
|
// DBTypeSQLite is the constant for SQLite database type
|
|
const DBTypeSQLite = "sqlite"
|
|
|
|
// NewDB creates a new database connection.
|
|
func NewDB(config DBConfig) (*DB, error) {
|
|
var conn *sql.DB
|
|
var err error
|
|
|
|
switch strings.ToLower(config.Type) {
|
|
case DBTypeSQLite:
|
|
conn, err = sql.Open("sqlite3", config.Connection)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open SQLite database: %w", err)
|
|
}
|
|
// Enable foreign keys
|
|
if _, err := conn.ExecContext(context.Background(), "PRAGMA foreign_keys = ON"); err != nil {
|
|
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
|
|
}
|
|
// Enable WAL mode for better concurrency
|
|
if _, err := conn.ExecContext(context.Background(), "PRAGMA journal_mode = WAL"); err != nil {
|
|
return nil, fmt.Errorf("failed to enable WAL mode: %w", err)
|
|
}
|
|
// Additional SQLite optimizations for throughput
|
|
if _, err := conn.ExecContext(context.Background(), "PRAGMA synchronous = NORMAL"); err != nil {
|
|
return nil, fmt.Errorf("failed to set synchronous mode: %w", err)
|
|
}
|
|
if _, err := conn.ExecContext(context.Background(), "PRAGMA cache_size = 10000"); err != nil {
|
|
return nil, fmt.Errorf("failed to set cache size: %w", err)
|
|
}
|
|
if _, err := conn.ExecContext(context.Background(), "PRAGMA temp_store = MEMORY"); err != nil {
|
|
return nil, fmt.Errorf("failed to set temp store: %w", err)
|
|
}
|
|
case "postgres":
|
|
connStr := buildPostgresConnectionString(config)
|
|
conn, err = sql.Open("postgres", connStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open PostgreSQL database: %w", err)
|
|
}
|
|
case "postgresql":
|
|
// Handle "postgresql" as alias for "postgres"
|
|
connStr := buildPostgresConnectionString(config)
|
|
conn, err = sql.Open("postgres", connStr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open PostgreSQL database: %w", err)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unsupported database type: %s", config.Type)
|
|
}
|
|
|
|
// Optimize connection pool for better throughput
|
|
conn.SetMaxOpenConns(50) // Increase max open connections
|
|
conn.SetMaxIdleConns(25) // Maintain idle connections
|
|
conn.SetConnMaxLifetime(5 * time.Minute) // Connection lifetime
|
|
conn.SetConnMaxIdleTime(2 * time.Minute) // Idle connection timeout
|
|
|
|
return &DB{conn: conn, dbType: strings.ToLower(config.Type)}, nil
|
|
}
|
|
|
|
func buildPostgresConnectionString(config DBConfig) string {
|
|
if config.Connection != "" {
|
|
return config.Connection
|
|
}
|
|
|
|
var connStr strings.Builder
|
|
connStr.WriteString("host=")
|
|
if config.Host != "" {
|
|
connStr.WriteString(config.Host)
|
|
} else {
|
|
connStr.WriteString("localhost")
|
|
}
|
|
|
|
if config.Port > 0 {
|
|
connStr.WriteString(fmt.Sprintf(" port=%d", config.Port))
|
|
} else {
|
|
connStr.WriteString(" port=5432")
|
|
}
|
|
|
|
if config.Username != "" {
|
|
connStr.WriteString(fmt.Sprintf(" user=%s", config.Username))
|
|
}
|
|
|
|
if config.Password != "" {
|
|
connStr.WriteString(fmt.Sprintf(" password=%s", config.Password))
|
|
}
|
|
|
|
if config.Database != "" {
|
|
connStr.WriteString(fmt.Sprintf(" dbname=%s", config.Database))
|
|
} else {
|
|
connStr.WriteString(" dbname=fetch_ml")
|
|
}
|
|
|
|
connStr.WriteString(" sslmode=disable")
|
|
return connStr.String()
|
|
}
|
|
|
|
// NewDBFromPath creates a new database from a file path (legacy constructor).
|
|
func NewDBFromPath(dbPath string) (*DB, error) {
|
|
return NewDB(DBConfig{
|
|
Type: DBTypeSQLite,
|
|
Connection: dbPath,
|
|
})
|
|
}
|
|
|
|
// Initialize creates database schema.
|
|
func (db *DB) Initialize(schema string) error {
|
|
if _, err := db.conn.ExecContext(context.Background(), schema); err != nil {
|
|
return fmt.Errorf("failed to initialize database: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Close closes the database connection.
|
|
func (db *DB) Close() error {
|
|
return db.conn.Close()
|
|
}
|