From 70d19d09d03c112744643f0cd496adf3d58b8766 Mon Sep 17 00:00:00 2001 From: sneak Date: Tue, 22 Jul 2025 13:35:19 +0200 Subject: [PATCH] latest --- .claude/settings.local.json | 34 -------- .gitignore | 1 + Makefile | 10 ++- go.mod | 4 + go.sum | 11 +++ internal/cli/info.go | 161 ++++++++++++++++++++++++++++++++++++ internal/cli/info_helper.go | 83 +++++++++++++++++++ internal/cli/root.go | 1 + 8 files changed, 270 insertions(+), 35 deletions(-) delete mode 100644 .claude/settings.local.json create mode 100644 internal/cli/info.go create mode 100644 internal/cli/info_helper.go diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index ae3133d..0000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(go mod why:*)", - "Bash(go list:*)", - "Bash(~/go/bin/govulncheck -mode=module .)", - "Bash(go test:*)", - "Bash(grep:*)", - "Bash(rg:*)", - "Bash(find:*)", - "Bash(make test:*)", - "Bash(go doc:*)", - "Bash(make fmt:*)", - "Bash(make:*)", - "Bash(golangci-lint run:*)", - "Bash(git add:*)", - "Bash(gofumpt:*)", - "Bash(git stash:*)", - "Bash(git commit:*)", - "Bash(git push:*)", - "Bash(golangci-lint:*)", - "Bash(git checkout:*)", - "Bash(ls:*)", - "WebFetch(domain:golangci-lint.run)", - "Bash(go:*)", - "WebFetch(domain:pkg.go.dev)", - "Bash(CGO_ENABLED=1 make fmt)", - "Bash(CGO_ENABLED=1 make test)", - "Bash(git merge:*)", - "Bash(git branch:*)" - ], - "deny": [] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index b349c81..79c2f3d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ cli.test vault.test *.test +settings.local.json diff --git a/Makefile b/Makefile index 56037f3..1e5a9ea 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,19 @@ export CGO_ENABLED=1 export DOCKER_HOST := ssh://root@ber1app1.local +# Version information +VERSION := 0.1.0 +GIT_COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown") +LDFLAGS := -X 'git.eeqj.de/sneak/secret/internal/cli.Version=$(VERSION)' \ + -X 'git.eeqj.de/sneak/secret/internal/cli.GitCommit=$(GIT_COMMIT)' + default: check build: ./secret # Simple build (no code signing needed) ./secret: - go build -v -o $@ cmd/secret/main.go + go build -v -ldflags "$(LDFLAGS)" -o $@ cmd/secret/main.go vet: go vet ./... @@ -21,6 +27,8 @@ fmt: lint: golangci-lint run --timeout 5m +check: build test + # Build Docker container docker: docker build -t sneak/secret . diff --git a/go.mod b/go.mod index 7bf2b42..841c3ee 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,11 @@ require ( github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.6 // indirect golang.org/x/sys v0.33.0 // indirect diff --git a/go.sum b/go.sum index 5f1485a..66b0089 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,10 @@ github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -66,6 +70,11 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/keybase/go-keychain v0.0.0-20230307172405-3e4884637dd1 h1:yi1W8qcFJ2plmaGJFN1npm0KQviWPMCtQOYuwDT6Swk= github.com/keybase/go-keychain v0.0.0-20230307172405-3e4884637dd1/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= @@ -119,6 +128,8 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= diff --git a/internal/cli/info.go b/internal/cli/info.go new file mode 100644 index 0000000..1a838e1 --- /dev/null +++ b/internal/cli/info.go @@ -0,0 +1,161 @@ +package cli + +import ( + "encoding/json" + "fmt" + "io" + "path/filepath" + "runtime" + "strings" + "time" + + "git.eeqj.de/sneak/secret/internal/vault" + "github.com/dustin/go-humanize" + "github.com/fatih/color" + "github.com/spf13/afero" + "github.com/spf13/cobra" +) + +// Version info - these are set at build time +var ( //nolint:gochecknoglobals // Set at build time + Version = "dev" //nolint:gochecknoglobals // Set at build time + GitCommit = "unknown" //nolint:gochecknoglobals // Set at build time +) + +// InfoOutput represents the system information for JSON output +type InfoOutput struct { + Version string `json:"version"` + GitCommit string `json:"gitCommit"` + Author string `json:"author"` + License string `json:"license"` + GoVersion string `json:"goVersion"` + DataDirectory string `json:"dataDirectory"` + CurrentVault string `json:"currentVault"` + NumVaults int `json:"numVaults"` + NumSecrets int `json:"numSecrets"` + TotalSize int64 `json:"totalSizeBytes"` + OldestSecret time.Time `json:"oldestSecret,omitempty"` + LatestSecret time.Time `json:"latestSecret,omitempty"` +} + +// newInfoCmd returns the info command +func newInfoCmd() *cobra.Command { + cli := NewCLIInstance() + + var jsonOutput bool + + cmd := &cobra.Command{ + Use: "info", + Short: "Display system information", + Long: "Display information about the secret system including version, vault statistics, and storage usage", + RunE: func(cmd *cobra.Command, _ []string) error { + return cli.Info(cmd, jsonOutput) + }, + } + + cmd.Flags().BoolVar(&jsonOutput, "json", false, "Output in JSON format") + + return cmd +} + +// Info displays system information +func (cli *Instance) Info(cmd *cobra.Command, jsonOutput bool) error { + info := InfoOutput{ + Version: Version, + GitCommit: GitCommit, + Author: "Jeffrey Paul ", + License: "WTFPL", + GoVersion: runtime.Version(), + DataDirectory: cli.stateDir, + } + + // Get current vault + currentVault, err := vault.GetCurrentVault(cli.fs, cli.stateDir) + if err == nil { + info.CurrentVault = currentVault.Name + } + + // Count vaults + vaultsDir := filepath.Join(cli.stateDir, "vaults.d") + vaultEntries, err := afero.ReadDir(cli.fs, vaultsDir) + if err == nil { + for _, entry := range vaultEntries { + if entry.IsDir() { + info.NumVaults++ + } + } + } + + // Gather statistics from all vaults + if info.NumVaults > 0 { + totalSecrets, totalSize, oldestTime, latestTime, _ := gatherVaultStats(cli.fs, vaultsDir) + info.NumSecrets = totalSecrets + info.TotalSize = totalSize + if !oldestTime.IsZero() { + info.OldestSecret = oldestTime + } + if !latestTime.IsZero() { + info.LatestSecret = latestTime + } + } + + if jsonOutput { + encoder := json.NewEncoder(cmd.OutOrStdout()) + encoder.SetIndent("", " ") + + return encoder.Encode(info) + } + + // Pretty print with colors and emoji + return prettyPrintInfo(cmd.OutOrStdout(), info) +} + +// prettyPrintInfo formats and prints the info in a pretty format +func prettyPrintInfo(w io.Writer, info InfoOutput) error { + const separatorLength = 40 + + bold := color.New(color.Bold) + green := color.New(color.FgGreen) + cyan := color.New(color.FgCyan) + yellow := color.New(color.FgYellow) + magenta := color.New(color.FgMagenta) + + _, _ = fmt.Fprintln(w) + _, _ = bold.Fprintln(w, "🔐 Secret System Information") + _, _ = fmt.Fprintln(w, strings.Repeat("─", separatorLength)) + + _, _ = fmt.Fprintf(w, "📦 Version: %s\n", green.Sprint(info.Version)) + _, _ = fmt.Fprintf(w, "🔧 Git Commit: %s\n", cyan.Sprint(info.GitCommit)) + _, _ = fmt.Fprintf(w, "👤 Author: %s\n", cyan.Sprint(info.Author)) + _, _ = fmt.Fprintf(w, "📜 License: %s\n", cyan.Sprint(info.License)) + _, _ = fmt.Fprintf(w, "🐹 Go Version: %s\n", cyan.Sprint(info.GoVersion)) + _, _ = fmt.Fprintf(w, "📁 Data Directory: %s\n", yellow.Sprint(info.DataDirectory)) + + if info.CurrentVault != "" { + _, _ = fmt.Fprintf(w, "🗄️ Current Vault: %s\n", magenta.Sprint(info.CurrentVault)) + } else { + _, _ = fmt.Fprintf(w, "🗄️ Current Vault: %s\n", color.RedString("(none)")) + } + + _, _ = fmt.Fprintln(w, strings.Repeat("─", separatorLength)) + + _, _ = fmt.Fprintf(w, "🗂️ Vaults: %s\n", bold.Sprint(info.NumVaults)) + _, _ = fmt.Fprintf(w, "🔑 Secrets: %s\n", bold.Sprint(info.NumSecrets)) + if info.TotalSize >= 0 { + //nolint:gosec // TotalSize is always >= 0 + _, _ = fmt.Fprintf(w, "💾 Total Size: %s\n", bold.Sprint(humanize.Bytes(uint64(info.TotalSize)))) + } else { + _, _ = fmt.Fprintf(w, "💾 Total Size: %s\n", bold.Sprint("0 B")) + } + + if !info.OldestSecret.IsZero() { + _, _ = fmt.Fprintf(w, "🕰️ Oldest Secret: %s\n", info.OldestSecret.Format("2006-01-02 15:04:05")) + } + if !info.LatestSecret.IsZero() { + _, _ = fmt.Fprintf(w, "✨ Latest Secret: %s\n", info.LatestSecret.Format("2006-01-02 15:04:05")) + } + + _, _ = fmt.Fprintln(w) + + return nil +} diff --git a/internal/cli/info_helper.go b/internal/cli/info_helper.go new file mode 100644 index 0000000..d4a57aa --- /dev/null +++ b/internal/cli/info_helper.go @@ -0,0 +1,83 @@ +package cli + +import ( + "path/filepath" + "time" + + "github.com/spf13/afero" +) + +// gatherVaultStats collects statistics from all vaults +func gatherVaultStats( + fs afero.Fs, + vaultsDir string, +) (totalSecrets int, totalSize int64, oldestTime, latestTime time.Time, err error) { + vaultEntries, err := afero.ReadDir(fs, vaultsDir) + if err != nil { + return 0, 0, time.Time{}, time.Time{}, err + } + + for _, vaultEntry := range vaultEntries { + if !vaultEntry.IsDir() { + continue + } + + vaultPath := filepath.Join(vaultsDir, vaultEntry.Name()) + secretsPath := filepath.Join(vaultPath, "secrets.d") + + // Count secrets in this vault + secretEntries, err := afero.ReadDir(fs, secretsPath) + if err != nil { + continue + } + + for _, secretEntry := range secretEntries { + if !secretEntry.IsDir() { + continue + } + + totalSecrets++ + secretPath := filepath.Join(secretsPath, secretEntry.Name()) + + // Get size and timestamps from all versions + versionsPath := filepath.Join(secretPath, "versions") + versionEntries, err := afero.ReadDir(fs, versionsPath) + if err != nil { + continue + } + + for _, versionEntry := range versionEntries { + if !versionEntry.IsDir() { + continue + } + + versionPath := filepath.Join(versionsPath, versionEntry.Name()) + + // Add size of encrypted data + dataPath := filepath.Join(versionPath, "data.age") + if stat, err := fs.Stat(dataPath); err == nil { + totalSize += stat.Size() + } + + // Add size of metadata + metaPath := filepath.Join(versionPath, "metadata.age") + if stat, err := fs.Stat(metaPath); err == nil { + totalSize += stat.Size() + } + + // Track timestamps + if stat, err := fs.Stat(versionPath); err == nil { + modTime := stat.ModTime() + if oldestTime.IsZero() || modTime.Before(oldestTime) { + oldestTime = modTime + } + if latestTime.IsZero() || modTime.After(latestTime) { + latestTime = modTime + } + } + } + } + } + + return totalSecrets, totalSize, oldestTime, latestTime, nil +} diff --git a/internal/cli/root.go b/internal/cli/root.go index d11c055..701e65f 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -41,6 +41,7 @@ func newRootCmd() *cobra.Command { cmd.AddCommand(newEncryptCmd()) cmd.AddCommand(newDecryptCmd()) cmd.AddCommand(newVersionCmd()) + cmd.AddCommand(newInfoCmd()) secret.Debug("newRootCmd completed")