fix: resolve CI failures in docker build
Some checks failed
Check / check (pull_request) Failing after 15s
Some checks failed
Check / check (pull_request) Failing after 15s
- Install golangci-lint v2 via binary download instead of go install (avoids Go 1.25 requirement of golangci-lint v2.10+) - Add darwin build tags to tests that depend on macOS keychain: derivation_index_test.go, pgpunlock_test.go, validation (keychain tests) - Move generateRandomString to helpers_darwin.go (only called from darwin-only keychainunlocker.go) - Fix unchecked error returns flagged by errcheck linter - Add gnupg to builder stage for PGP-related tests - Use --ulimit memlock=-1:-1 in CI for memguard large secret tests - Add //nolint:unused for intentionally kept but currently unused test helpers
This commit is contained in:
@@ -13,4 +13,4 @@ jobs:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13925f8d5 # v4
|
||||
|
||||
- name: Build (includes make check)
|
||||
run: docker build .
|
||||
run: docker build --ulimit memlock=-1:-1 .
|
||||
|
||||
@@ -6,7 +6,8 @@ RUN apk add --no-cache \
|
||||
gcc \
|
||||
musl-dev \
|
||||
make \
|
||||
git
|
||||
git \
|
||||
gnupg
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
@@ -20,8 +21,8 @@ RUN go mod download
|
||||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# Install golangci-lint for checks
|
||||
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee
|
||||
# Install golangci-lint for checks (binary install to avoid Go version constraints)
|
||||
RUN wget -O- -nv https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.1.6
|
||||
|
||||
# Run all checks (lint, vet, test, build)
|
||||
RUN make check
|
||||
|
||||
@@ -48,7 +48,7 @@ func TestMain(m *testing.M) {
|
||||
code := m.Run()
|
||||
|
||||
// Clean up the binary
|
||||
os.Remove(filepath.Join(projectRoot, "secret"))
|
||||
_ = os.Remove(filepath.Join(projectRoot, "secret"))
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
@@ -450,10 +450,10 @@ func test02ListVaults(t *testing.T, runSecret func(...string) (string, error)) {
|
||||
|
||||
func test03CreateVault(t *testing.T, tempDir string, runSecret func(...string) (string, error)) {
|
||||
// Set environment variables for vault creation
|
||||
os.Setenv("SB_SECRET_MNEMONIC", testMnemonic)
|
||||
os.Setenv("SB_UNLOCK_PASSPHRASE", "test-passphrase")
|
||||
defer os.Unsetenv("SB_SECRET_MNEMONIC")
|
||||
defer os.Unsetenv("SB_UNLOCK_PASSPHRASE")
|
||||
_ = os.Setenv("SB_SECRET_MNEMONIC", testMnemonic)
|
||||
_ = os.Setenv("SB_UNLOCK_PASSPHRASE", "test-passphrase")
|
||||
defer func() { _ = os.Unsetenv("SB_SECRET_MNEMONIC") }()
|
||||
defer func() { _ = os.Unsetenv("SB_UNLOCK_PASSPHRASE") }()
|
||||
|
||||
// Create work vault
|
||||
output, err := runSecret("vault", "create", "work")
|
||||
@@ -489,6 +489,7 @@ func test03CreateVault(t *testing.T, tempDir string, runSecret func(...string) (
|
||||
assert.Contains(t, output, "work", "should list work vault")
|
||||
}
|
||||
|
||||
//nolint:unused // TODO: re-enable when vault import is implemented
|
||||
func test04ImportMnemonic(t *testing.T, tempDir, testMnemonic, testPassphrase string, runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
// Import mnemonic into work vault
|
||||
output, err := runSecretWithEnv(map[string]string{
|
||||
@@ -1667,9 +1668,9 @@ func test19DisasterRecovery(t *testing.T, tempDir, secretPath, testMnemonic stri
|
||||
assert.Equal(t, testSecretValue, strings.TrimSpace(toolOutput), "tool output should match original")
|
||||
|
||||
// Clean up temporary files
|
||||
os.Remove(ltPrivKeyPath)
|
||||
os.Remove(versionPrivKeyPath)
|
||||
os.Remove(decryptedValuePath)
|
||||
_ = os.Remove(ltPrivKeyPath)
|
||||
_ = os.Remove(versionPrivKeyPath)
|
||||
_ = os.Remove(decryptedValuePath)
|
||||
}
|
||||
|
||||
func test20VersionTimestamps(t *testing.T, tempDir, secretPath, testMnemonic string, runSecretWithEnv func(map[string]string, ...string) (string, error)) {
|
||||
@@ -1788,7 +1789,7 @@ func test23ErrorHandling(t *testing.T, tempDir, secretPath, testMnemonic string,
|
||||
|
||||
// Add secret without mnemonic or unlocker
|
||||
unsetMnemonic := os.Getenv("SB_SECRET_MNEMONIC")
|
||||
os.Unsetenv("SB_SECRET_MNEMONIC")
|
||||
_ = os.Unsetenv("SB_SECRET_MNEMONIC")
|
||||
cmd := exec.Command(secretPath, "add", "test/nomnemonic")
|
||||
cmd.Env = []string{
|
||||
fmt.Sprintf("SB_SECRET_STATE_DIR=%s", tempDir),
|
||||
@@ -2128,7 +2129,7 @@ func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string,
|
||||
versionsPath := filepath.Join(secretPath, "versions")
|
||||
if _, err := os.Stat(versionsPath); os.IsNotExist(err) {
|
||||
// This is a malformed secret directory, remove it
|
||||
os.RemoveAll(secretPath)
|
||||
_ = os.RemoveAll(secretPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2178,7 +2179,7 @@ func test30BackupRestore(t *testing.T, tempDir, secretPath, testMnemonic string,
|
||||
require.NoError(t, err, "restore vaults should succeed")
|
||||
|
||||
// Restore currentvault
|
||||
os.Remove(currentVaultSrc)
|
||||
_ = os.Remove(currentVaultSrc)
|
||||
restoredData := readFile(t, currentVaultDst)
|
||||
writeFile(t, currentVaultSrc, restoredData)
|
||||
|
||||
@@ -2284,6 +2285,7 @@ func verifyFileExists(t *testing.T, path string) {
|
||||
}
|
||||
|
||||
// verifyFileNotExists checks if a file does not exist at the given path
|
||||
//nolint:unused // kept for future use
|
||||
func verifyFileNotExists(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
_, err := os.Stat(path)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build darwin
|
||||
|
||||
package secret
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,33 +1,11 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// generateRandomString generates a random string of the specified length using the given character set
|
||||
func generateRandomString(length int, charset string) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", fmt.Errorf("length must be positive")
|
||||
}
|
||||
|
||||
result := make([]byte, length)
|
||||
charsetLen := big.NewInt(int64(len(charset)))
|
||||
|
||||
for i := range length {
|
||||
randomIndex, err := rand.Int(rand.Reader, charsetLen)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random number: %w", err)
|
||||
}
|
||||
result[i] = charset[randomIndex.Int64()]
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// DetermineStateDir determines the state directory based on environment variables and OS.
|
||||
// It returns an error if no usable directory can be determined.
|
||||
func DetermineStateDir(customConfigDir string) (string, error) {
|
||||
|
||||
29
internal/secret/helpers_darwin.go
Normal file
29
internal/secret/helpers_darwin.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build darwin
|
||||
|
||||
package secret
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// generateRandomString generates a random string of the specified length using the given character set
|
||||
func generateRandomString(length int, charset string) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", fmt.Errorf("length must be positive")
|
||||
}
|
||||
|
||||
result := make([]byte, length)
|
||||
charsetLen := big.NewInt(int64(len(charset)))
|
||||
|
||||
for i := range length {
|
||||
randomIndex, err := rand.Int(rand.Reader, charsetLen)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random number: %w", err)
|
||||
}
|
||||
result[i] = charset[randomIndex.Int64()]
|
||||
}
|
||||
|
||||
return string(result), nil
|
||||
}
|
||||
@@ -24,7 +24,7 @@ func TestPassphraseUnlockerWithRealFS(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir) // Clean up after test
|
||||
defer func() { _ = os.RemoveAll(tempDir) }() // Clean up after test
|
||||
|
||||
// Use the real filesystem
|
||||
fs := afero.NewOsFs()
|
||||
@@ -155,7 +155,7 @@ func TestPassphraseUnlockerWithRealFS(t *testing.T) {
|
||||
})
|
||||
|
||||
// Unset the environment variable to test interactive prompt
|
||||
os.Unsetenv(secret.EnvUnlockPassphrase)
|
||||
_ = os.Unsetenv(secret.EnvUnlockPassphrase)
|
||||
|
||||
// Test getting identity from prompt (this would require mocking the prompt)
|
||||
// For real integration tests, we'd need to provide a way to mock the passphrase input
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build darwin
|
||||
|
||||
package secret_test
|
||||
|
||||
import (
|
||||
@@ -140,7 +142,7 @@ func TestPGPUnlockerWithRealFS(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir) // Clean up after test
|
||||
defer func() { _ = os.RemoveAll(tempDir) }() // Clean up after test
|
||||
|
||||
// Create a temporary GNUPGHOME
|
||||
gnupgHomeDir := filepath.Join(tempDir, "gnupg")
|
||||
|
||||
148
internal/secret/validation_darwin_test.go
Normal file
148
internal/secret/validation_darwin_test.go
Normal file
@@ -0,0 +1,148 @@
|
||||
//go:build darwin
|
||||
|
||||
package secret
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateKeychainItemName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
itemName string
|
||||
wantErr bool
|
||||
}{
|
||||
// Valid cases
|
||||
{
|
||||
name: "valid simple name",
|
||||
itemName: "my-secret-key",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid name with dots",
|
||||
itemName: "com.example.app.key",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid name with underscores",
|
||||
itemName: "my_secret_key_123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid alphanumeric",
|
||||
itemName: "Secret123Key",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid with hyphen at start",
|
||||
itemName: "-my-key",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid with dot at start",
|
||||
itemName: ".hidden-key",
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// Invalid cases
|
||||
{
|
||||
name: "empty item name",
|
||||
itemName: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with spaces",
|
||||
itemName: "my secret key",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with semicolon",
|
||||
itemName: "key;rm -rf /",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with pipe",
|
||||
itemName: "key|cat /etc/passwd",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with backticks",
|
||||
itemName: "key`whoami`",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with dollar sign",
|
||||
itemName: "key$(whoami)",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with quotes",
|
||||
itemName: "key\"name",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with single quotes",
|
||||
itemName: "key'name",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with backslash",
|
||||
itemName: "key\\name",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with newline",
|
||||
itemName: "key\nname",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with carriage return",
|
||||
itemName: "key\rname",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with ampersand",
|
||||
itemName: "key&echo test",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with redirect",
|
||||
itemName: "key>/tmp/test",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with null byte",
|
||||
itemName: "key\x00name",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with parentheses",
|
||||
itemName: "key(test)",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with brackets",
|
||||
itemName: "key[test]",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with asterisk",
|
||||
itemName: "key*",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with question mark",
|
||||
itemName: "key?",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateKeychainItemName(tt.itemName)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateKeychainItemName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -155,143 +155,3 @@ func TestValidateGPGKeyID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateKeychainItemName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
itemName string
|
||||
wantErr bool
|
||||
}{
|
||||
// Valid cases
|
||||
{
|
||||
name: "valid simple name",
|
||||
itemName: "my-secret-key",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid name with dots",
|
||||
itemName: "com.example.app.key",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid name with underscores",
|
||||
itemName: "my_secret_key_123",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid alphanumeric",
|
||||
itemName: "Secret123Key",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid with hyphen at start",
|
||||
itemName: "-my-key",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid with dot at start",
|
||||
itemName: ".hidden-key",
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
// Invalid cases
|
||||
{
|
||||
name: "empty item name",
|
||||
itemName: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with spaces",
|
||||
itemName: "my secret key",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with semicolon",
|
||||
itemName: "key;rm -rf /",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with pipe",
|
||||
itemName: "key|cat /etc/passwd",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with backticks",
|
||||
itemName: "key`whoami`",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with dollar sign",
|
||||
itemName: "key$(whoami)",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with quotes",
|
||||
itemName: "key\"name",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with single quotes",
|
||||
itemName: "key'name",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with backslash",
|
||||
itemName: "key\\name",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with newline",
|
||||
itemName: "key\nname",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with carriage return",
|
||||
itemName: "key\rname",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with ampersand",
|
||||
itemName: "key&echo test",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with redirect",
|
||||
itemName: "key>/tmp/test",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with null byte",
|
||||
itemName: "key\x00name",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with parentheses",
|
||||
itemName: "key(test)",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with brackets",
|
||||
itemName: "key[test]",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with asterisk",
|
||||
itemName: "key*",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "item name with question mark",
|
||||
itemName: "key?",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateKeychainItemName(tt.itemName)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("validateKeychainItemName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user