Skip to content
Merged
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
38 changes: 37 additions & 1 deletion cmd/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ package main

import (
"fmt"
"os"
"runtime/pprof"
"time"

"github.com/spf13/cobra"

Expand All @@ -33,6 +36,7 @@ var lintCmd = &cobra.Command{
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
vaultPassFile, _ := cmd.Flags().GetString("vault-password-file")
cpuProfile, _ := cmd.Flags().GetString("cpu-profile")
// Determine config file path
configFilePath := defaultConfigFile
if len(args) > 0 {
Expand All @@ -47,7 +51,37 @@ var lintCmd = &cobra.Command{

fmt.Println("🔍 Validating configuration...")

config_lint.Validate(cfg)
// Start CPU profiling if requested
if cpuProfile != "" {
f, err := os.Create(cpuProfile)
if err != nil {
return fmt.Errorf("could not create CPU profile: %w", err)
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
fmt.Printf("Warning: failed to close profile file: %v\n", closeErr)
}
}()

if err := pprof.StartCPUProfile(f); err != nil {
return fmt.Errorf("could not start CPU profile: %w", err)
}
defer pprof.StopCPUProfile()

fmt.Printf("📊 CPU profiling enabled, output will be saved to: %s\n", cpuProfile)
}

// Time the validation
start := time.Now()
err = config_lint.ValidateWithError(cfg)
duration := time.Since(start)

if err != nil {
fmt.Printf("❌ Configuration validation failed in %v\n", duration)
return err
}

fmt.Printf("✅ Configuration validation completed in %v\n", duration)

return nil
},
Expand All @@ -56,6 +90,8 @@ var lintCmd = &cobra.Command{
func init() {
// Add the vault password file flag
lintCmd.Flags().String("vault-password-file", "", "Path to the vault password file for decrypting variables")
// Add the CPU profiling flag
lintCmd.Flags().String("cpu-profile", "", "Enable CPU profiling and save output to the specified file")
// Add the lint command to the root command
rootCmd.AddCommand(lintCmd)
}
15 changes: 15 additions & 0 deletions internal/config_lint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,21 @@ func Validate(cfg *config.Config) {
fmt.Println("✅ All configurations are valid")
}

// ValidateWithError checks the loaded configuration for errors and returns an error if any are found.
// This version does not call os.Exit() and is suitable for use with profiling.
func ValidateWithError(cfg *config.Config) error {
errorsByFile := Lint(cfg)
if len(errorsByFile) > 0 {
reportAllErrors(errorsByFile)
return fmt.Errorf("validation failed with %d error(s)", len(errorsByFile))
}
keys := cfg.Loaded.GetVariables().GetAllKeys()
fmt.Printf("📚 Total Variables: %d\n", len(keys))
fmt.Println("")
fmt.Println("✅ All configurations are valid")
return nil
}

func Lint(cfg *config.Config) map[string][]error {
// This function is a placeholder for the linting logic.
// Currently, it only validates cross-references between objects.
Expand Down
37 changes: 31 additions & 6 deletions internal/vars/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/hex"
"errors"
"strings"
"sync"

"golang.org/x/crypto/pbkdf2"
)
Expand Down Expand Up @@ -38,6 +39,8 @@ type CipherKey struct {
// VaultDecryptor handles Ansible Vault decryption
type VaultDecryptor struct {
password string
cache map[string]*CipherKey
mutex sync.RWMutex
}

// NewVaultDecryptor creates a new vault decryptor with the given password
Expand All @@ -47,7 +50,10 @@ func NewVaultDecryptor(password string) *VaultDecryptor {
password = strings.ReplaceAll(password, "\n", "")
password = strings.ReplaceAll(password, "\r", "")
password = strings.ReplaceAll(password, "\t", "")
return &VaultDecryptor{password: password}
return &VaultDecryptor{
password: password,
cache: make(map[string]*CipherKey),
}
}

// IsVaultEncrypted checks if a variable value is Ansible Vault encrypted
Expand Down Expand Up @@ -76,7 +82,7 @@ func (vd *VaultDecryptor) Decrypt(vaultData string) (string, error) {
return "", err
}
// Generate keys
key := generateCipherKey(vd.password, salt)
key := vd.generateCipherKey(salt)

// Verify checksum
if !isChecksumValid(checkSum, encryptedData, key.HMACKey) {
Expand Down Expand Up @@ -134,21 +140,40 @@ func parseVaultData(vaultData string) (salt, checkSum, data []byte, err error) {
return salt, checkSum, data, nil
}

// generateCipherKey generates cipher keys from password and salt
func generateCipherKey(password string, salt []byte) *CipherKey {
// generateCipherKey generates cipher keys from password and salt with caching
func (vd *VaultDecryptor) generateCipherKey(salt []byte) *CipherKey {
// Create cache key from salt (password is already stored in the struct)
cacheKey := hex.EncodeToString(salt)

// Try to get from cache first (read lock)
vd.mutex.RLock()
if cached, exists := vd.cache[cacheKey]; exists {
vd.mutex.RUnlock()
return cached
}
vd.mutex.RUnlock()

// Generate new cipher key
k := pbkdf2.Key(
[]byte(password),
[]byte(vd.password),
salt,
iteration,
(cipherKeyLength + hmacKeyLength + ivLength),
sha256.New,
)

return &CipherKey{
cipherKey := &CipherKey{
Key: k[:cipherKeyLength],
HMACKey: k[cipherKeyLength:(cipherKeyLength + hmacKeyLength)],
IV: k[(cipherKeyLength + hmacKeyLength):(cipherKeyLength + hmacKeyLength + ivLength)],
}

// Store in cache (write lock)
vd.mutex.Lock()
vd.cache[cacheKey] = cipherKey
vd.mutex.Unlock()

return cipherKey
}

// isChecksumValid validates HMAC checksum
Expand Down