Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
|
|
@ -0,0 +1,435 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/config"
|
||||
)
|
||||
|
||||
const (
|
||||
// MinJWTSecretLength is the minimum required length for JWT secrets (256 bits)
|
||||
MinJWTSecretLength = 32
|
||||
|
||||
// RecommendedJWTSecretLength is the recommended length for JWT secrets (512 bits)
|
||||
RecommendedJWTSecretLength = 64
|
||||
|
||||
// MinEntropyBits is the minimum Shannon entropy in bits per character
|
||||
// For reference: random base64 has ~6 bits/char, we require minimum 4.0
|
||||
MinEntropyBits = 4.0
|
||||
|
||||
// MinProductionEntropyBits is the minimum entropy required for production
|
||||
MinProductionEntropyBits = 4.5
|
||||
|
||||
// MaxRepeatingCharacters is the maximum allowed consecutive repeating characters
|
||||
MaxRepeatingCharacters = 3
|
||||
)
|
||||
|
||||
// WeakSecrets contains common weak/default secrets that should never be used
|
||||
var WeakSecrets = []string{
|
||||
"secret",
|
||||
"password",
|
||||
"changeme",
|
||||
"change-me",
|
||||
"change_me",
|
||||
"12345",
|
||||
"123456",
|
||||
"1234567",
|
||||
"12345678",
|
||||
"123456789",
|
||||
"1234567890",
|
||||
"default",
|
||||
"test",
|
||||
"testing",
|
||||
"admin",
|
||||
"administrator",
|
||||
"root",
|
||||
"qwerty",
|
||||
"qwertyuiop",
|
||||
"letmein",
|
||||
"welcome",
|
||||
"monkey",
|
||||
"dragon",
|
||||
"master",
|
||||
"sunshine",
|
||||
"princess",
|
||||
"football",
|
||||
"starwars",
|
||||
"baseball",
|
||||
"superman",
|
||||
"iloveyou",
|
||||
"trustno1",
|
||||
"hello",
|
||||
"abc123",
|
||||
"password123",
|
||||
"admin123",
|
||||
"guest",
|
||||
"user",
|
||||
"demo",
|
||||
"sample",
|
||||
"example",
|
||||
}
|
||||
|
||||
// DangerousPatterns contains patterns that indicate a secret should be changed
|
||||
var DangerousPatterns = []string{
|
||||
"change",
|
||||
"replace",
|
||||
"update",
|
||||
"modify",
|
||||
"sample",
|
||||
"example",
|
||||
"todo",
|
||||
"fixme",
|
||||
"temp",
|
||||
"temporary",
|
||||
}
|
||||
|
||||
// CredentialValidator validates credentials and secrets for security issues
|
||||
type CredentialValidator interface {
|
||||
ValidateJWTSecret(secret string, environment string) error
|
||||
ValidateAllCredentials(cfg *config.Config) error
|
||||
}
|
||||
|
||||
type credentialValidator struct{}
|
||||
|
||||
// NewCredentialValidator creates a new credential validator
|
||||
func NewCredentialValidator() CredentialValidator {
|
||||
return &credentialValidator{}
|
||||
}
|
||||
|
||||
// ValidateJWTSecret validates JWT secret strength and security
|
||||
// CWE-798: Comprehensive validation to prevent hard-coded/weak credentials
|
||||
func (v *credentialValidator) ValidateJWTSecret(secret string, environment string) error {
|
||||
// Check minimum length
|
||||
if len(secret) < MinJWTSecretLength {
|
||||
return fmt.Errorf(
|
||||
"JWT secret is too short (%d characters). Minimum required: %d characters (256 bits). "+
|
||||
"Generate a secure secret with: openssl rand -base64 64",
|
||||
len(secret),
|
||||
MinJWTSecretLength,
|
||||
)
|
||||
}
|
||||
|
||||
// Check for common weak secrets (case-insensitive)
|
||||
secretLower := strings.ToLower(secret)
|
||||
for _, weak := range WeakSecrets {
|
||||
if secretLower == weak || strings.Contains(secretLower, weak) {
|
||||
return fmt.Errorf(
|
||||
"JWT secret cannot contain common weak value: '%s'. "+
|
||||
"Generate a secure secret with: openssl rand -base64 64",
|
||||
weak,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dangerous patterns indicating default/placeholder values
|
||||
for _, pattern := range DangerousPatterns {
|
||||
if strings.Contains(secretLower, pattern) {
|
||||
return fmt.Errorf(
|
||||
"JWT secret contains suspicious pattern '%s' which suggests it's a placeholder. "+
|
||||
"Generate a secure secret with: openssl rand -base64 64",
|
||||
pattern,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for repeating character patterns (e.g., "aaaa", "1111")
|
||||
if err := checkRepeatingPatterns(secret); err != nil {
|
||||
return fmt.Errorf(
|
||||
"JWT secret validation failed: %s. "+
|
||||
"Generate a secure secret with: openssl rand -base64 64",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
// Check for sequential patterns (e.g., "abcd", "1234")
|
||||
if hasSequentialPattern(secret) {
|
||||
return fmt.Errorf(
|
||||
"JWT secret contains sequential patterns (e.g., 'abcd', '1234') which reduces entropy. "+
|
||||
"Generate a secure secret with: openssl rand -base64 64",
|
||||
)
|
||||
}
|
||||
|
||||
// Calculate Shannon entropy
|
||||
entropy := calculateShannonEntropy(secret)
|
||||
minEntropy := MinEntropyBits
|
||||
if environment == "production" {
|
||||
minEntropy = MinProductionEntropyBits
|
||||
}
|
||||
|
||||
if entropy < minEntropy {
|
||||
return fmt.Errorf(
|
||||
"JWT secret has insufficient entropy: %.2f bits/char (minimum: %.1f bits/char for %s). "+
|
||||
"The secret appears to have low randomness. "+
|
||||
"Generate a secure secret with: openssl rand -base64 64",
|
||||
entropy,
|
||||
minEntropy,
|
||||
environment,
|
||||
)
|
||||
}
|
||||
|
||||
// In production, enforce stricter requirements
|
||||
if environment == "production" {
|
||||
// Check recommended length for production
|
||||
if len(secret) < RecommendedJWTSecretLength {
|
||||
return fmt.Errorf(
|
||||
"JWT secret is too short for production environment (%d characters). "+
|
||||
"Recommended: %d characters (512 bits). "+
|
||||
"Generate a secure secret with: openssl rand -base64 64",
|
||||
len(secret),
|
||||
RecommendedJWTSecretLength,
|
||||
)
|
||||
}
|
||||
|
||||
// Check for sufficient character complexity
|
||||
if !hasSufficientComplexity(secret) {
|
||||
return fmt.Errorf(
|
||||
"JWT secret has insufficient complexity for production. It should contain a mix of uppercase, lowercase, " +
|
||||
"digits, and special characters (at least 3 types). Generate a secure secret with: openssl rand -base64 64",
|
||||
)
|
||||
}
|
||||
|
||||
// Validate base64-like characteristics (recommended generation method)
|
||||
if !looksLikeBase64(secret) {
|
||||
return fmt.Errorf(
|
||||
"JWT secret does not appear to be randomly generated (expected base64-like characteristics). "+
|
||||
"Generate a secure secret with: openssl rand -base64 64",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAllCredentials validates all credentials in the configuration
|
||||
func (v *credentialValidator) ValidateAllCredentials(cfg *config.Config) error {
|
||||
var errors []string
|
||||
|
||||
// Validate JWT Secret
|
||||
if err := v.ValidateJWTSecret(cfg.App.JWTSecret, cfg.App.Environment); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("JWT Secret validation failed: %s", err.Error()))
|
||||
}
|
||||
|
||||
// In production, ensure other critical configs are not using defaults/placeholders
|
||||
if cfg.App.Environment == "production" {
|
||||
// Check Meilisearch API key
|
||||
if cfg.Meilisearch.APIKey == "" {
|
||||
errors = append(errors, "Meilisearch API key must be set in production")
|
||||
} else if containsDangerousPattern(cfg.Meilisearch.APIKey) {
|
||||
errors = append(errors, "Meilisearch API key appears to be a placeholder/default value")
|
||||
}
|
||||
|
||||
// Check database hosts are not using localhost
|
||||
for _, host := range cfg.Database.Hosts {
|
||||
if strings.Contains(strings.ToLower(host), "localhost") || host == "127.0.0.1" {
|
||||
errors = append(errors, "Database hosts should not use localhost in production")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Check cache host is not localhost
|
||||
if strings.Contains(strings.ToLower(cfg.Cache.Host), "localhost") || cfg.Cache.Host == "127.0.0.1" {
|
||||
errors = append(errors, "Cache host should not use localhost in production")
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("credential validation failed:\n - %s", strings.Join(errors, "\n - "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateShannonEntropy calculates the Shannon entropy of a string in bits per character
|
||||
// Shannon entropy measures the randomness/unpredictability of data
|
||||
// Formula: H(X) = -Σ(p(x) * log2(p(x))) where p(x) is the probability of character x
|
||||
func calculateShannonEntropy(s string) float64 {
|
||||
if len(s) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Count character frequencies
|
||||
frequencies := make(map[rune]int)
|
||||
for _, char := range s {
|
||||
frequencies[char]++
|
||||
}
|
||||
|
||||
// Calculate entropy
|
||||
var entropy float64
|
||||
length := float64(len(s))
|
||||
|
||||
for _, count := range frequencies {
|
||||
probability := float64(count) / length
|
||||
entropy -= probability * math.Log2(probability)
|
||||
}
|
||||
|
||||
return entropy
|
||||
}
|
||||
|
||||
// hasSufficientComplexity checks if the secret has a good mix of character types
|
||||
// Requires at least 3 out of 4 character types for production
|
||||
func hasSufficientComplexity(secret string) bool {
|
||||
var (
|
||||
hasUpper bool
|
||||
hasLower bool
|
||||
hasDigit bool
|
||||
hasSpecial bool
|
||||
)
|
||||
|
||||
for _, char := range secret {
|
||||
switch {
|
||||
case unicode.IsUpper(char):
|
||||
hasUpper = true
|
||||
case unicode.IsLower(char):
|
||||
hasLower = true
|
||||
case unicode.IsDigit(char):
|
||||
hasDigit = true
|
||||
default:
|
||||
hasSpecial = true
|
||||
}
|
||||
}
|
||||
|
||||
// Require at least 3 out of 4 character types
|
||||
count := 0
|
||||
if hasUpper {
|
||||
count++
|
||||
}
|
||||
if hasLower {
|
||||
count++
|
||||
}
|
||||
if hasDigit {
|
||||
count++
|
||||
}
|
||||
if hasSpecial {
|
||||
count++
|
||||
}
|
||||
|
||||
return count >= 3
|
||||
}
|
||||
|
||||
// checkRepeatingPatterns checks for excessive repeating characters
|
||||
func checkRepeatingPatterns(s string) error {
|
||||
if len(s) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
repeatCount := 1
|
||||
lastChar := rune(s[0])
|
||||
|
||||
for _, char := range s[1:] {
|
||||
if char == lastChar {
|
||||
repeatCount++
|
||||
if repeatCount > MaxRepeatingCharacters {
|
||||
return fmt.Errorf(
|
||||
"contains %d consecutive repeating characters ('%c'), maximum allowed: %d",
|
||||
repeatCount,
|
||||
lastChar,
|
||||
MaxRepeatingCharacters,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
repeatCount = 1
|
||||
lastChar = char
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasSequentialPattern detects common sequential patterns
|
||||
func hasSequentialPattern(s string) bool {
|
||||
if len(s) < 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for at least 4 consecutive sequential characters
|
||||
for i := 0; i < len(s)-3; i++ {
|
||||
// Check ascending sequence (e.g., "abcd", "1234")
|
||||
if s[i+1] == s[i]+1 && s[i+2] == s[i]+2 && s[i+3] == s[i]+3 {
|
||||
return true
|
||||
}
|
||||
// Check descending sequence (e.g., "dcba", "4321")
|
||||
if s[i+1] == s[i]-1 && s[i+2] == s[i]-2 && s[i+3] == s[i]-3 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// looksLikeBase64 checks if the string has base64-like characteristics
|
||||
// Base64 uses: A-Z, a-z, 0-9, +, /, and = for padding
|
||||
func looksLikeBase64(s string) bool {
|
||||
if len(s) < MinJWTSecretLength {
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
hasUpper bool
|
||||
hasLower bool
|
||||
hasDigit bool
|
||||
validChars int
|
||||
)
|
||||
|
||||
// Base64 valid characters
|
||||
for _, char := range s {
|
||||
switch {
|
||||
case char >= 'A' && char <= 'Z':
|
||||
hasUpper = true
|
||||
validChars++
|
||||
case char >= 'a' && char <= 'z':
|
||||
hasLower = true
|
||||
validChars++
|
||||
case char >= '0' && char <= '9':
|
||||
hasDigit = true
|
||||
validChars++
|
||||
case char == '+' || char == '/' || char == '=' || char == '-' || char == '_':
|
||||
validChars++
|
||||
default:
|
||||
// Invalid character for base64
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Should have good mix of character types typical of base64
|
||||
charTypesCount := 0
|
||||
if hasUpper {
|
||||
charTypesCount++
|
||||
}
|
||||
if hasLower {
|
||||
charTypesCount++
|
||||
}
|
||||
if hasDigit {
|
||||
charTypesCount++
|
||||
}
|
||||
|
||||
// Base64 typically has at least uppercase, lowercase, and digits
|
||||
// Also check that it doesn't look like a repeated pattern
|
||||
if charTypesCount < 3 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for repeated patterns (e.g., "AbCd12!@" repeated)
|
||||
// If the string has low unique character count relative to its length, it's probably not random
|
||||
uniqueChars := make(map[rune]bool)
|
||||
for _, char := range s {
|
||||
uniqueChars[char] = true
|
||||
}
|
||||
|
||||
// Random base64 should have at least 50% unique characters for strings over 32 chars
|
||||
uniqueRatio := float64(len(uniqueChars)) / float64(len(s))
|
||||
return uniqueRatio >= 0.4 // At least 40% unique characters
|
||||
}
|
||||
|
||||
// containsDangerousPattern checks if a string contains any dangerous patterns
|
||||
func containsDangerousPattern(value string) bool {
|
||||
valueLower := strings.ToLower(value)
|
||||
for _, pattern := range DangerousPatterns {
|
||||
if strings.Contains(valueLower, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Simplified comprehensive test for JWT secret validation
|
||||
func TestJWTSecretValidation(t *testing.T) {
|
||||
validator := NewCredentialValidator()
|
||||
|
||||
// Good secrets - these should pass
|
||||
goodSecrets := []struct {
|
||||
name string
|
||||
secret string
|
||||
env string
|
||||
}{
|
||||
{
|
||||
name: "Good 32-char for dev",
|
||||
secret: "ima7xR+9nT0Yz0jKVu/QwtkqdAaU+3Ki",
|
||||
env: "development",
|
||||
},
|
||||
{
|
||||
name: "Good 64-char for prod",
|
||||
secret: "1WDduocStecRuIv+Us1t/RnYDoW1ZcEEbU+H+WykJG+IT5WnijzBb8uUPzGKju+D",
|
||||
env: "production",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range goodSecrets {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateJWTSecret(tt.secret, tt.env)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for valid secret, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Bad secrets - these should fail
|
||||
badSecrets := []struct {
|
||||
name string
|
||||
secret string
|
||||
env string
|
||||
mustContain string
|
||||
}{
|
||||
{
|
||||
name: "Too short",
|
||||
secret: "short",
|
||||
env: "development",
|
||||
mustContain: "too short",
|
||||
},
|
||||
{
|
||||
name: "Common weak - password",
|
||||
secret: "password-is-not-secure-but-32char",
|
||||
env: "development",
|
||||
mustContain: "common weak value",
|
||||
},
|
||||
{
|
||||
name: "Dangerous pattern",
|
||||
secret: "please-change-this-ima7xR+9nT0Yz",
|
||||
env: "development",
|
||||
mustContain: "suspicious pattern",
|
||||
},
|
||||
{
|
||||
name: "Repeating characters",
|
||||
secret: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
env: "development",
|
||||
mustContain: "consecutive repeating characters",
|
||||
},
|
||||
{
|
||||
name: "Sequential pattern",
|
||||
secret: "abcdefghijklmnopqrstuvwxyzabcdef",
|
||||
env: "development",
|
||||
mustContain: "sequential patterns",
|
||||
},
|
||||
{
|
||||
name: "Low entropy",
|
||||
secret: "abababababababababababababababab",
|
||||
env: "development",
|
||||
mustContain: "insufficient entropy",
|
||||
},
|
||||
{
|
||||
name: "Prod too short",
|
||||
secret: "ima7xR+9nT0Yz0jKVu/QwtkqdAaU+3Ki",
|
||||
env: "production",
|
||||
mustContain: "too short for production",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range badSecrets {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateJWTSecret(tt.secret, tt.env)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error containing '%s', got no error", tt.mustContain)
|
||||
} else if !contains(err.Error(), tt.mustContain) {
|
||||
t.Errorf("Expected error containing '%s', got: %v", tt.mustContain, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
|
||||
}
|
||||
|
||||
func findSubstring(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -0,0 +1,535 @@
|
|||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCalculateShannonEntropy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
minBits float64
|
||||
maxBits float64
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
minBits: 0,
|
||||
maxBits: 0,
|
||||
expected: "should have 0 entropy",
|
||||
},
|
||||
{
|
||||
name: "All same character",
|
||||
input: "aaaaaaaaaa",
|
||||
minBits: 0,
|
||||
maxBits: 0,
|
||||
expected: "should have very low entropy",
|
||||
},
|
||||
{
|
||||
name: "Low entropy - repeated pattern",
|
||||
input: "abcabcabcabc",
|
||||
minBits: 1.5,
|
||||
maxBits: 2.0,
|
||||
expected: "should have low entropy",
|
||||
},
|
||||
{
|
||||
name: "Medium entropy - simple password",
|
||||
input: "Password123",
|
||||
minBits: 3.0,
|
||||
maxBits: 4.5,
|
||||
expected: "should have medium entropy",
|
||||
},
|
||||
{
|
||||
name: "High entropy - random base64",
|
||||
input: "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx",
|
||||
minBits: 4.0,
|
||||
maxBits: 6.0,
|
||||
expected: "should have high entropy",
|
||||
},
|
||||
{
|
||||
name: "Very high entropy - long random base64",
|
||||
input: "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR",
|
||||
minBits: 4.5,
|
||||
maxBits: 6.5,
|
||||
expected: "should have very high entropy",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
entropy := calculateShannonEntropy(tt.input)
|
||||
if entropy < tt.minBits || entropy > tt.maxBits {
|
||||
t.Errorf("%s: got %.2f bits/char, expected between %.1f and %.1f", tt.expected, entropy, tt.minBits, tt.maxBits)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasSufficientComplexity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Only lowercase",
|
||||
input: "abcdefghijklmnop",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Only uppercase",
|
||||
input: "ABCDEFGHIJKLMNOP",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Only digits",
|
||||
input: "1234567890",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Lowercase + uppercase",
|
||||
input: "AbCdEfGhIjKl",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Lowercase + digits",
|
||||
input: "abc123def456",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Uppercase + digits",
|
||||
input: "ABC123DEF456",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Lowercase + uppercase + digits",
|
||||
input: "Abc123Def456",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Lowercase + uppercase + special",
|
||||
input: "AbC+DeF/GhI=",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Lowercase + digits + special",
|
||||
input: "abc123+def456/",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "All four types",
|
||||
input: "Abc123+Def456/",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 string",
|
||||
input: "K8vN2mP9sQ4tR7wY3zA6b+xK8vN2mP9sQ4tR7wY3zA6b=",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := hasSufficientComplexity(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("hasSufficientComplexity(%q) = %v, expected %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRepeatingPatterns(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
shouldErr bool
|
||||
}{
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Single character",
|
||||
input: "a",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "No repeating",
|
||||
input: "abcdefgh",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Two repeating (ok)",
|
||||
input: "aabcdeef",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Three repeating (ok)",
|
||||
input: "aaabcdeee",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Four repeating (error)",
|
||||
input: "aaaabcde",
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "Five repeating (error)",
|
||||
input: "aaaaabcde",
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "Multiple groups of three (ok)",
|
||||
input: "aaabbbccc",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Repeating in middle (error)",
|
||||
input: "abcdddddef",
|
||||
shouldErr: true,
|
||||
},
|
||||
{
|
||||
name: "Repeating at end (error)",
|
||||
input: "abcdefgggg",
|
||||
shouldErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := checkRepeatingPatterns(tt.input)
|
||||
if (err != nil) != tt.shouldErr {
|
||||
t.Errorf("checkRepeatingPatterns(%q) error = %v, shouldErr = %v", tt.input, err, tt.shouldErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasSequentialPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Too short",
|
||||
input: "abc",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "No sequential",
|
||||
input: "acegikmo",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Ascending sequence - abcd",
|
||||
input: "xyzabcdefg",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Descending sequence - dcba",
|
||||
input: "xyzdcbafg",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Ascending digits - 1234",
|
||||
input: "abc1234def",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Descending digits - 4321",
|
||||
input: "abc4321def",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Random characters",
|
||||
input: "xK8vN2mP9sQ4",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Base64-like",
|
||||
input: "K8vN2mP9sQ4tR7wY3zA6b",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := hasSequentialPattern(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("hasSequentialPattern(%q) = %v, expected %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLooksLikeBase64(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "Empty string",
|
||||
input: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Too short",
|
||||
input: "abc",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Only lowercase",
|
||||
input: "abcdefghijklmnopqrstuvwxyzabcdef",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "Real base64",
|
||||
input: "K8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6b=",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 without padding",
|
||||
input: "K8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6b",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Base64 with URL-safe chars",
|
||||
input: "K8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6b-_",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Generated secret",
|
||||
input: "xK8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6bxK8vN2mP9sQ4tR7wY3zA6b",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Simple password",
|
||||
input: "Password123!Password123!Password123!",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := looksLikeBase64(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("looksLikeBase64(%q) = %v, expected %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateJWTSecret(t *testing.T) {
|
||||
validator := NewCredentialValidator()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
secret string
|
||||
environment string
|
||||
shouldErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "Too short - 20 chars",
|
||||
secret: "12345678901234567890",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "too short",
|
||||
},
|
||||
{
|
||||
name: "Minimum length - 32 chars (acceptable for dev)",
|
||||
secret: "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx",
|
||||
environment: "development",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Common weak secret - contains password",
|
||||
secret: "my-password-is-secure-123456789012",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "common weak value",
|
||||
},
|
||||
{
|
||||
name: "Common weak secret - secret",
|
||||
secret: "secretsecretsecretsecretsecretsec",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "common weak value",
|
||||
},
|
||||
{
|
||||
name: "Common weak secret - contains 12345",
|
||||
secret: "abcd12345efghijklmnopqrstuvwxyz",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "common weak value",
|
||||
},
|
||||
{
|
||||
name: "Dangerous pattern - change",
|
||||
secret: "please-change-this-j8EJm9ZKnuTYxcVK",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "suspicious pattern",
|
||||
},
|
||||
{
|
||||
name: "Dangerous pattern - sample",
|
||||
secret: "sample-secret-j8EJm9ZKnuTYxcVKQ",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "suspicious pattern",
|
||||
},
|
||||
{
|
||||
name: "Repeating characters",
|
||||
secret: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "consecutive repeating characters",
|
||||
},
|
||||
{
|
||||
name: "Sequential pattern - abcd",
|
||||
secret: "abcdefghijklmnopqrstuvwxyzabcdef",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "sequential patterns",
|
||||
},
|
||||
{
|
||||
name: "Sequential pattern - 1234",
|
||||
secret: "12345678901234567890123456789012",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "sequential patterns",
|
||||
},
|
||||
{
|
||||
name: "Low entropy secret",
|
||||
secret: "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP",
|
||||
environment: "development",
|
||||
shouldErr: true,
|
||||
errContains: "insufficient entropy",
|
||||
},
|
||||
{
|
||||
name: "Good secret - base64 style (dev)",
|
||||
secret: "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx",
|
||||
environment: "development",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Good secret - longer (dev)",
|
||||
secret: "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR",
|
||||
environment: "development",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Production - too short (32 chars)",
|
||||
secret: "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx",
|
||||
environment: "production",
|
||||
shouldErr: true,
|
||||
errContains: "too short for production",
|
||||
},
|
||||
{
|
||||
name: "Production - insufficient complexity",
|
||||
secret: "abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz01",
|
||||
environment: "production",
|
||||
shouldErr: true,
|
||||
errContains: "insufficient complexity",
|
||||
},
|
||||
{
|
||||
name: "Production - low entropy pattern",
|
||||
secret: strings.Repeat("AbCd12!@", 8), // 64 chars but repetitive
|
||||
environment: "production",
|
||||
shouldErr: true,
|
||||
errContains: "insufficient entropy",
|
||||
},
|
||||
{
|
||||
name: "Production - good secret",
|
||||
secret: "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR",
|
||||
environment: "production",
|
||||
shouldErr: false,
|
||||
},
|
||||
{
|
||||
name: "Production - excellent secret with padding",
|
||||
secret: "7mK2nP8sR4wT6xZ3bA5cxK7mN1oQ9uS4vY2zA6bxK7mN1oQ9uS4vY2zA6b+W0E=",
|
||||
environment: "production",
|
||||
shouldErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validator.ValidateJWTSecret(tt.secret, tt.environment)
|
||||
|
||||
if tt.shouldErr {
|
||||
if err == nil {
|
||||
t.Errorf("ValidateJWTSecret() expected error containing %q, got no error", tt.errContains)
|
||||
} else if !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("ValidateJWTSecret() error = %q, should contain %q", err.Error(), tt.errContains)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("ValidateJWTSecret() unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateJWTSecret_EdgeCases(t *testing.T) {
|
||||
validator := NewCredentialValidator()
|
||||
|
||||
t.Run("Secret with mixed weak patterns", func(t *testing.T) {
|
||||
secret := "password123admin" // Contains multiple weak patterns
|
||||
err := validator.ValidateJWTSecret(secret, "development")
|
||||
if err == nil {
|
||||
t.Error("Expected error for secret containing weak patterns, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Secret exactly at minimum length", func(t *testing.T) {
|
||||
// 32 characters exactly
|
||||
secret := "j8EJm9/ZKnuTYxcVKQK/NWcrt1Drgzx"
|
||||
err := validator.ValidateJWTSecret(secret, "development")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for 32-char secret in development, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Secret exactly at recommended length", func(t *testing.T) {
|
||||
// 64 characters exactly - using real random base64
|
||||
secret := "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFir"
|
||||
err := validator.ValidateJWTSecret(secret, "production")
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for 64-char secret in production, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Benchmark tests to ensure validation is performant
|
||||
func BenchmarkCalculateShannonEntropy(b *testing.B) {
|
||||
secret := "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR"
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
calculateShannonEntropy(secret)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkValidateJWTSecret(b *testing.B) {
|
||||
validator := NewCredentialValidator()
|
||||
secret := "PKiQCYBT+AxkksUbC+F5NJsQBG+GDRvlc/5d+240xljW2uVtzsz0uqv0sjCJFirR"
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = validator.ValidateJWTSecret(secret, "production")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package validator
|
||||
|
||||
// ProvideCredentialValidator provides a credential validator for dependency injection
|
||||
func ProvideCredentialValidator() CredentialValidator {
|
||||
return NewCredentialValidator()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue