mirror of
https://github.com/skoelle/kctl-tui.git
synced 2026-09-17 20:10:24 +00:00
Phase 2 hardening: binary secret handling, tool checks, input validation, verbose flag, diff scroll
This commit is contained in:
+28
-5
@@ -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
|
||||
}
|
||||
|
||||
@@ -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'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user