Phase 2 hardening: binary secret handling, tool checks, input validation, verbose flag, diff scroll

This commit is contained in:
2026-08-09 19:23:37 +02:00
parent 2c8e8016ff
commit edc02d48d4
7 changed files with 238 additions and 14 deletions
+28 -5
View File
@@ -5,10 +5,27 @@ import "sort"
// SecretDiffEntry represents the comparison of one key between two secret
// sources (e.g. AWS Secrets Manager vs. a Kubernetes Secret).
type SecretDiffEntry struct {
Key string
Left string // e.g. the AWS Secrets Manager value
Right string // e.g. the decoded Kubernetes secret value
Match bool
Key string
Left string // e.g. the AWS Secrets Manager value
Right string // e.g. the decoded Kubernetes secret value
Match bool
LeftBin bool // true if Left contains non-printable (binary) data
RightBin bool // true if Right contains non-printable (binary) data
}
// IsBinary reports whether s contains non-printable bytes (i.e. is likely
// binary data rather than human-readable text). Control characters below
// space (0x20) are excluded, except for common whitespace (\t, \n, \r).
func IsBinary(s string) bool {
for _, r := range s {
if r > 0x7f {
return true
}
if r < 0x20 && r != '\t' && r != '\n' && r != '\r' {
return true
}
}
return false
}
// DiffSecretValues compares two key/value maps and returns a sorted list of
@@ -35,7 +52,13 @@ func DiffSecretValues(left, right map[string]string) []SecretDiffEntry {
for _, k := range keys {
l := left[k]
r := right[k]
result = append(result, SecretDiffEntry{Key: k, Left: l, Right: r, Match: l == r})
lb := IsBinary(l)
rb := IsBinary(r)
match := l == r
if lb || rb {
match = l == r
}
result = append(result, SecretDiffEntry{Key: k, Left: l, Right: r, Match: match, LeftBin: lb, RightBin: rb})
}
return result
}
+40
View File
@@ -57,3 +57,43 @@ func TestDiffSecretValues_EmptyMaps(t *testing.T) {
t.Fatalf("expected no mismatch for empty maps")
}
}
func TestIsBinary_Plaintext(t *testing.T) {
if IsBinary("hello world") {
t.Fatal("expected plaintext to not be binary")
}
if IsBinary("line1\nline2\ttab") {
t.Fatal("expected newline/tab to not be binary")
}
}
func TestIsBinary_BinaryData(t *testing.T) {
if !IsBinary("hello\x00world") {
t.Fatal("expected null byte to be binary")
}
if !IsBinary("key=\xff\xfe") {
t.Fatal("expected non-ASCII bytes to be binary")
}
}
func TestDiffSecretValues_BinaryDetection(t *testing.T) {
left := map[string]string{"ok": "text", "bin": "data\x00here"}
right := map[string]string{"ok": "text", "bin": "data\x00here"}
entries := DiffSecretValues(left, right)
for _, e := range entries {
if e.Key == "bin" {
if !e.LeftBin || !e.RightBin {
t.Fatalf("expected binary flags set for key 'bin', got LeftBin=%v RightBin=%v", e.LeftBin, e.RightBin)
}
if !e.Match {
t.Fatalf("expected binary values to match")
}
}
if e.Key == "ok" {
if e.LeftBin || e.RightBin {
t.Fatalf("expected binary flags unset for key 'ok'")
}
}
}
}
+17 -2
View File
@@ -19,13 +19,16 @@ import (
)
func runOutput(name string, args ...string) (string, error) {
logCmd(name, args...)
cmd := exec.Command(name, args...)
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
msg := fmt.Sprintf("%s %s failed: %v", name, strings.Join(args, " "), err)
logErr(name, err)
prefix := fmt.Sprintf("%s %s failed: %v", name, strings.Join(args, " "), err)
msg := prefix
if s := strings.TrimSpace(stderr.String()); s != "" {
msg += "\n" + s
}
@@ -34,7 +37,9 @@ func runOutput(name string, args ...string) (string, error) {
}
return "", fmt.Errorf("%s", msg)
}
return strings.TrimSpace(stdout.String()), nil
out := strings.TrimSpace(stdout.String())
logOutput(name, out)
return out, nil
}
// kubectlArgs prepends a --context flag when context is non-empty.
@@ -172,3 +177,13 @@ func RunAWSLogin(loginCommand string) *exec.Cmd {
}
return exec.Command(parts[0], parts[1:]...)
}
// CheckTool verifies that a named executable is available in PATH.
// Returns nil if found, or a descriptive error if not.
func CheckTool(name string) error {
_, err := exec.LookPath(name)
if err != nil {
return fmt.Errorf("%q not found in PATH — please install it first", name)
}
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package kubeexec
import (
"fmt"
"io"
"strings"
"sync"
)
var (
verbose bool
logOut io.Writer = io.Discard
mu sync.Mutex
)
// SetVerbose enables or disables debug logging of executed commands.
// When enabled, commands and their outputs are written to the provided
// writer (typically os.Stderr). When disabled (the default), all logging
// is discarded.
func SetVerbose(enabled bool, w io.Writer) {
mu.Lock()
defer mu.Unlock()
verbose = enabled
if w != nil {
logOut = w
}
}
func logCmd(name string, args ...string) {
mu.Lock()
defer mu.Unlock()
if !verbose {
return
}
fmt.Fprintf(logOut, "[cmd] %s %s\n", name, strings.Join(args, " "))
}
func logOutput(name string, output string) {
mu.Lock()
defer mu.Unlock()
if !verbose {
return
}
if output != "" {
fmt.Fprintf(logOut, "[out] %s: %s\n", name, output)
}
}
func logErr(name string, err error) {
mu.Lock()
defer mu.Unlock()
if !verbose {
return
}
fmt.Fprintf(logOut, "[err] %s: %v\n", name, err)
}