Files
mikeandClaude Opus 5 59da8f376c Check the staged diff for credentials before push commits
`push` runs `git add --all .`, so anything lying in the project gets
committed, and with `mirror = true` it reaches a public server in the
same breath. It is the one action in mgsh that cannot be undone: a
deleted server repository comes back from an archive, a published
credential does not.

The staged diff is now scanned before the commit is made -- private keys,
GitHub/GitLab/Slack/AWS/PyPI tokens, and credential-shaped assignments --
and a hit is shown with file and line before asking whether to continue.
Declining leaves the changes staged but uncommitted, so removing the file
and adding a .gitignore entry is all it takes.

The hard part is not detection but silence. A scanner that cries wolf
gets answered with a reflexive "y" and stops being a safety net, so
values that are plainly environment references, dotted identifiers,
constant names, template slots or masked stand-ins are filtered out. A
test scans mgsh's own README and mgshrc.example -- both full of
credential-shaped text -- and fails if either would trip the check. It
caught the documentation for this very feature, which is why the README
describes the sample output instead of reproducing it.

For a line that legitimately looks like a credential there is
`mgsh:allow`, which suppresses that one line; `secretscan = off` turns
the check off entirely. Only an explicit "off" does that -- a typo in the
setting leaves the safety net in place, which is what the new falsy()
is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:17:30 +02:00

106 lines
2.0 KiB
Go

package main
import (
"bytes"
"net"
"os"
"path/filepath"
"strings"
)
func word(words []string, i int) string {
if i < len(words) {
return words[i]
}
return ""
}
// splitLines splits s on newlines, dropping a trailing newline and returning
// nil for empty input.
func splitLines(s string) []string {
s = strings.TrimRight(s, "\n")
if s == "" {
return nil
}
return strings.Split(s, "\n")
}
func isDir(p string) bool {
fi, err := os.Stat(p)
return err == nil && fi.IsDir()
}
// truthy reports whether a config string means "on" (1/true/yes/on).
func truthy(s string) bool {
switch strings.ToLower(strings.TrimSpace(s)) {
case "1", "true", "yes", "on":
return true
}
return false
}
// falsy reports whether a config string explicitly means "off". It is not the
// negation of truthy: for a setting that defaults to on, an unset value or a
// typo must leave it on, and only a deliberate "off" may switch it off.
func falsy(s string) bool {
switch strings.ToLower(strings.TrimSpace(s)) {
case "0", "false", "no", "off":
return true
}
return false
}
func fileExists(p string) bool {
fi, err := os.Stat(p)
return err == nil && !fi.IsDir()
}
func filesEqual(a, b string) bool {
da, ea := os.ReadFile(a)
db, eb := os.ReadFile(b)
if ea != nil || eb != nil {
return false
}
return bytes.Equal(da, db)
}
func copyFile(src, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}
mode := os.FileMode(0644)
if fi, err := os.Stat(src); err == nil {
mode = fi.Mode()
}
return os.WriteFile(dst, data, mode)
}
func resolveIP() string {
h, err := os.Hostname()
if err != nil {
return ""
}
addrs, err := net.LookupHost(h)
if err != nil || len(addrs) == 0 {
return ""
}
for _, a := range addrs {
if ip := net.ParseIP(a); ip != nil && ip.To4() != nil {
return a
}
}
return addrs[0]
}
func shortHostname() string {
h, _ := os.Hostname()
if i := strings.IndexByte(h, '.'); i >= 0 {
h = h[:i]
}
return h
}